Python Tutorial
Assertions and Debugging in Python
Bugs are inevitable; finding them quickly is a skill. Python gives you assert statements to check assumptions while developing, a built-in debugger (pdb, started with breakpoint()), IDE debuggers with breakpoints and variable inspection, and helpful tools for tracing and timing code.
This lesson covers assertions and their limits, a systematic debugging process, pdb commands, IDE debugging, and quick diagnostic techniques.
The assert Statement
assert condition, "message" raises AssertionError if the condition is false. Use it for internal invariants that should never be false if the code is correct ("the discount is never above 100%") and in tests (pytest uses plain assert). Do not use assert to validate user input or enforce security: Python removes all asserts when run with -O (optimised mode).
A Debugging Process
Reproduce the bug reliably with the smallest input; read the full traceback; form a hypothesis; inspect actual values (print, logging or debugger) to confirm or reject it; fix the cause, not the symptom; then add a test so the bug never returns. Explaining the code to someone else (or a rubber duck) is surprisingly effective.
pdb and breakpoint()
Call breakpoint() anywhere to pause and open the interactive debugger. Useful commands: n (next line), s (step into), c (continue), l (list code), p expr (print), pp (pretty print), w (where — stack trace), u/d (move up/down the stack), b line (set breakpoint), q (quit). python -m pdb script.py starts a script under the debugger.
IDE Debuggers and Quick Tools
VS Code and PyCharm offer visual breakpoints, conditional breakpoints, watch expressions and step-through execution. For quick checks, print(f"{value=}") shows a variable with its name, pprint formats nested data, traceback.print_exc() prints the current exception, and time.perf_counter() measures slow sections.
Examples
Assertions for internal invariants
def apply_discount(price, percent):
assert 0 <= percent <= 100, f"invalid discount {percent}%"
discounted = price * (100 - percent) / 100
assert 0 <= discounted <= price, "discount produced an impossible price"
return discounted
print(apply_discount(1000, 20))
try:
apply_discount(1000, 150)
except AssertionError as e:
print("AssertionError:", e)
800.0
AssertionError: invalid discount 150%
Finding a bug by inspecting values
from pprint import pprint
def average_marks(students):
total = 0
for s in students:
total += s["marks"]
print(f"{total=} {len(students)=}") # quick inspection
return total / len(students)
students = [{"name": "Asha", "marks": 90}, {"name": "Ravi", "marks": 70}]
print(average_marks(students))
data = {"course": "Python", "students": students, "tags": ["beginner", "backend"]}
pprint(data, width=60)
total=160 len(students)=2
80.0
{'course': 'Python',
'students': [{'marks': 90, 'name': 'Asha'},
{'marks': 70, 'name': 'Ravi'}],
'tags': ['beginner', 'backend']}
Using the pdb debugger
def calculate_total(items):
total = 0
for price, qty in items:
breakpoint() # execution pauses here
total += price * qty
return total
calculate_total([(100, 2), (50, 3)])
# In the (Pdb) prompt:
# (Pdb) p price, qty -> (100, 2)
# (Pdb) p total -> 0
# (Pdb) n -> runs the next line
# (Pdb) p total -> 200
# (Pdb) c -> continue to the next breakpoint
# (Pdb) q -> quit
> main.py(5)calculate_total()
-> total += price * qty
(Pdb) p price, qty
(100, 2)
Timing a slow section
import time
start = time.perf_counter()
squares = [n * n for n in range(1_000_000)]
elapsed = time.perf_counter() - start
print(len(squares), "squares computed in under a second:", elapsed < 1)
1000000 squares computed in under a second: True
Common Mistakes
- Using assert to validate user input or permissions — asserts disappear with python -O.
- Writing assert (condition, "message") with parentheses — a non-empty tuple is always true.
- Changing code randomly until the bug disappears instead of confirming the cause.
- Leaving breakpoint() or debug prints in committed code.
Key Points to Remember
- assert checks internal assumptions during development and in tests, not user input.
- Debug systematically: reproduce, read the traceback, hypothesise, inspect, fix, test.
- breakpoint() opens pdb: n, s, c, p, l, w, q.
- IDE debuggers, f"{x=}", pprint and perf_counter speed up diagnosis.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.