Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
🟡 Advanced Python  ·  Lesson 29

Testing and Debugging in Python

In this lesson: Find and fix bugs in Python — print debugging, pdb, assert and unittest basics with examples. In Hindi & English.

Why testing and debugging matter

Debugging is finding out why a program behaves wrongly. Testing is writing small programs that check your code automatically, so the same bug never comes back. Professional developers spend more time on these two skills than on writing new code.

Step 1: read the traceback from the bottom

Python – traceback_demo.py
def average(marks):
    return sum(marks) / len(marks)

def report(student):
    avg = average(student["marks"])
    return f"{student['name']}: {avg:.1f}"

print(report({"name": "Asha", "marks": [91, 88]}))
print(report({"name": "Ravi", "marks": []}))
Asha: 89.5 Traceback (most recent call last): File "~/project/traceback_demo.py", line 9, in <module> print(report({"name": "Ravi", "marks": []})) ~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "~/project/traceback_demo.py", line 5, in report avg = average(student["marks"]) File "~/project/traceback_demo.py", line 2, in average return sum(marks) / len(marks) ~~~~~~~~~~~^~~~~~~~~~~~ ZeroDivisionError: division by zero
  1. Last line = error type and message: ZeroDivisionError: division by zero.
  2. Line above it = the exact line that failed (inside average).
  3. Read upwards to see who called it: report → line 9 of your script.
  4. Ask: what value made this fail? Here marks was an empty list.
ErrorUsual cause
NameErrorSpelling mistake or variable used before assignment
TypeErrorWrong type, e.g. "5" + 3 or missing argument
KeyError / IndexErrorKey not in dict / index past the end of a list
AttributeErrorCalling a method on the wrong object, often None
IndentationErrorMixed tabs/spaces or missing indent

Step 2: print() vs logging

print is fine for a quick check, but in real programs use logging: you can switch messages on/off by level, add time stamps and write to a file – without deleting lines later.

Python – logging_demo.py
import logging

logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(funcName)s: %(message)s")
log = logging.getLogger(__name__)

def apply_discount(price, percent):
    log.debug("raw input price=%r percent=%r", price, percent)   # hidden at INFO level
    if not 0 <= percent <= 50:
        log.warning("discount %s%% looks wrong, using 0", percent)
        percent = 0
    final = round(price * (100 - percent) / 100, 2)
    log.info("final price %s", final)
    return final

apply_discount(1200, 10)
apply_discount(1200, 90)
INFO apply_discount: final price 1080.0 WARNING apply_discount: discount 90% looks wrong, using 0 INFO apply_discount: final price 1200.0
LevelUse for
DEBUGDetailed values while developing
INFONormal milestones (“order saved”)
WARNINGSomething odd but the program continues
ERRORAn operation failed
CRITICALThe program cannot continue

Step 3: pause the program with breakpoint()

Put breakpoint() on any line. When Python reaches it, the program pauses and opens the debugger in the terminal. You can print variables, run the next line and continue.

Python – debug_me.py
def grade(marks):
    total = sum(marks)
    breakpoint()            # execution stops here
    percent = total / len(marks)
    return "A" if percent >= 85 else "B"

print(grade([91, 88, 79]))
> ~/project/debug_me.py(3)grade() -> breakpoint() # execution stops here (Pdb) p total 258 (Pdb) p len(marks) 3 (Pdb) n > ~/project/debug_me.py(4)grade() -> percent = total / len(marks) (Pdb) n > ~/project/debug_me.py(5)grade() -> return "A" if percent >= 85 else "B" (Pdb) p percent 86.0 (Pdb) c A
CommandMeaning
p xprint the value of x
nrun the next line
sstep into a function call
lshow code around the current line
ccontinue until the next breakpoint
qquit
💡 VS Code debugger

In VS Code click left of a line number to set a red breakpoint, then press F5 → “Python File”. The Variables panel shows every value and you can step with F10/F11 – no commands to remember.

Step 4: assert – the smallest test

Python – assert_demo.py
def to_paise(rupees):
    return round(rupees * 100)

assert to_paise(10) == 1000
assert to_paise(0.1 + 0.2) == 30          # floats are tricky – round() saves us
assert to_paise(99.99) == 9999
print("all checks passed")
all checks passed

If an assert condition is false, Python stops with AssertionError. Use asserts in tests and for “this should never happen” checks – not for validating user input (asserts can be disabled with python -O).

Step 5: automated tests with pytest

Here is a small billing function for a shop. The rule says: coupon SAVE10 works on bills of ₹500 or more, delivery is free from ₹1,000.

Python – bill.py
def bill_total(items, coupon=None):
    """items: list of (price, qty). SAVE10 gives 10% off on bills of Rs 500 or more."""
    subtotal = sum(price * qty for price, qty in items)
    if coupon == "SAVE10" and subtotal > 500:
        subtotal = subtotal * 0.9
    delivery = 0 if subtotal >= 1000 else 40
    return round(subtotal + delivery)

Tests are normal functions whose names start with test_, in files named test_*.py. Install pytest once (pip install pytest) and write:

Python – test_bill.py
import pytest
from bill import bill_total

def test_small_order_pays_delivery():
    assert bill_total([(100, 2)]) == 240

def test_free_delivery_from_1000():
    assert bill_total([(500, 2)]) == 1000

def test_coupon_on_exactly_500():
    assert bill_total([(250, 2)], coupon="SAVE10") == 490   # 450 + 40 delivery

@pytest.mark.parametrize("items, expected", [
    ([], 40),
    ([(99.5, 2)], 239),
    ([(300, 1), (200, 1)], 540),
])
def test_many_cases(items, expected):
    assert bill_total(items) == expected
..F... [100%] =================================== FAILURES =================================== __________________________ test_coupon_on_exactly_500 __________________________ def test_coupon_on_exactly_500(): > assert bill_total([(250, 2)], coupon="SAVE10") == 490 # 450 + 40 delivery ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E AssertionError: assert 540 == 490 E + where 540 = bill_total([(250, 2)], coupon='SAVE10') test_bill.py:11: AssertionError =========================== short test summary info ============================ FAILED test_bill.py::test_coupon_on_exactly_500 - AssertionError: assert 540 ... 1 failed, 5 passed in 0.02s

One test fails, and pytest shows exactly why: for ₹500 with the coupon we expected 490 but got 540. The bug is subtotal > 500; the rule says “500 or more”. Change it to >= 500 and run again:

Python – bill.py (fixed), then run python -m pytest -q again
def bill_total(items, coupon=None):
    """items: list of (price, qty). SAVE10 gives 10% off on bills of Rs 500 or more."""
    subtotal = sum(price * qty for price, qty in items)
    if coupon == "SAVE10" and subtotal >= 500:
        subtotal = subtotal * 0.9
    delivery = 0 if subtotal >= 1000 else 40
    return round(subtotal + delivery)
...... [100%] 6 passed in 0.01s
💡 Test edge cases

Bugs live at the edges: empty lists, exactly 500, exactly 1,000, decimals, zero, negative numbers. A good test suite checks each boundary once.

Testing that errors are raised

Python – test_bank.py
import pytest

def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError("amount must be positive")
    if amount > balance:
        raise ValueError("insufficient balance")
    return balance - amount

def test_normal_withdraw():
    assert withdraw(1000, 300) == 700

def test_overdraw_is_blocked():
    with pytest.raises(ValueError, match="insufficient"):
        withdraw(100, 500)
.. [100%] 2 passed in 0.00s

Built-in alternative: unittest

Python – test_pin.py
import unittest

def is_valid_pin(pin):
    return len(pin) == 6 and pin.isdigit() and pin[0] != "0"

class PinTests(unittest.TestCase):
    def test_valid(self):
        self.assertTrue(is_valid_pin("302001"))

    def test_invalid(self):
        for pin in ["02001", "0123456", "30200A", ""]:
            with self.subTest(pin=pin):
                self.assertFalse(is_valid_pin(pin))

if __name__ == "__main__":
    unittest.main(verbosity=2)
test_invalid (__main__.PinTests.test_invalid) ... ok test_valid (__main__.PinTests.test_valid) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK

unittest needs no installation and is used in many older projects. pytest can also run unittest tests, so you can mix both.

A debugging routine that always works

  1. Reproduce the bug with the smallest input.
  2. Read the traceback bottom-up.
  3. Inspect values with logging, breakpoint() or the VS Code debugger.
  4. Form one guess and test it – change one thing at a time.
  5. Write a test that fails because of the bug.
  6. Fix the code until the test passes, then run all tests.

Practice tasks

  1. Fix average() above so an empty list returns 0, and write a pytest test for it.
  2. Add logging to a file app.log with time stamps (%(asctime)s).
  3. Write 5 pytest tests for a function is_valid_mobile(number) (10 digits, starts 6–9).
  4. Use breakpoint() to find why "10" + str(5) gives 105 in a total.

Summary

  • Read tracebacks from the bottom: error type → failing line → callers.
  • Use logging levels instead of scattered prints.
  • breakpoint() or the VS Code debugger lets you pause and inspect values.
  • pytest: files test_*.py, functions test_*, plain assert; use parametrize and pytest.raises.
  • Every fixed bug should get a test so it never returns.

Frequently Asked Questions

pytest or unittest – which should I learn?
Learn pytest for new projects: tests are shorter, error messages are clearer and plugins are many. Know unittest basics because older projects and some interviews use it; pytest runs both.
How many tests are enough?
Test every important rule and each boundary (empty, zero, limits, wrong input). Aim for tests that would catch a real bug rather than a high coverage number.
← Back to Python Tutorial
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

💻 Live Code Editor

This page's programs are ready here — run them, edit them, and learn. No installation needed.
Powered by OneCompiler. The code loads into the editor automatically — press Run to see the output. If the editor does not open, open it in a new tab.
Next topicPython Best Practices Your progress: 29 / 66 (44%) · Lesson 30 / 66 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free