Lesson 24: Testing & Debugging Like a Pro
assert, pytest, the debugger, and logging — write code that proves itself.
Tests Are Not Optional
A professional program ships with tests that prove it works. Every time you change code, tests catch what you broke — instantly. Without tests, you are trusting "it worked when I tried it once".
This is the mindset shift that separates hobby code from production software. Companies ship with test suites that run on every commit — a failing test blocks the release. This lesson gives you that professional toolkit.
What You'll Learn in This Lesson
- Write quick checks with
assert - Build real tests with pytest
- Debug interactively with
breakpoint() - Log professionally with the
loggingmodule
assert — The Quick Check
assert condition, "message" raises AssertionError if the condition is False:
assert len("python") == 6, "expected six characters"
print("Assertion passed")
Great for sanity checks and quick validations. But asserts vanish with python -O — for real tests, use pytest.
Assertions are the grammar of tests. Every test you will ever write is, at heart, a collection of assertions: "this equals that", "this raises that", "this is True".
pytest — The Professional Standard
Tests are plain functions starting with test_ containing asserts. pytest discovers and runs them automatically:
# test_price.py
def price_after_tax(price, tax_rate=0.18):
return round(price * (1 + tax_rate), 2)
def test_normal_price():
assert price_after_tax(100) == 118.0
def test_zero_price():
assert price_after_tax(0) == 0.0
def test_high_tax():
assert price_after_tax(100, 0.5) == 150.0
Run with pytest:
collected 3 items
test_price.py ..... [100%]
============================= 3 passed in 0.02s =============================
Useful flags: pytest -v (verbose), pytest -k name (run a subset), pytest -x (stop on first failure).
Mental model: each test is a referee with a checklist — green for pass, red for fail. The suite is your safety net: change code, run tests, and the net catches any broken behavior instantly.
Testing exceptions with pytest:
import pytest
def test_division_by_zero():
with pytest.raises(ZeroDivisionError):
result = 1 / 0
The Debugger — Better than print()
print() debugging works, but the interactive debugger is far more powerful:
def mystery(a, b):
result = a * b
breakpoint() # execution PAUSES here
return result + 1
When breakpoint() runs, you drop into an interactive prompt:
| Command | Meaning |
|---|---|
n |
Next line |
s |
Step into a function |
c |
Continue to the next breakpoint |
p var |
Print a variable's value |
q |
Quit |
Mental model:
breakpoint()is a pause button that freezes the program mid-flight so you can inspect every variable.
Why the debugger beats print(): you can inspect any variable at the pause point without editing code and re-running; you can step line by line; and you can continue from where you stopped. print() requires guessing what to print and re-running each time.
Logging — Professional Print
The logging module writes timestamped, leveled messages to console or files — far more useful than print() in production:
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logging.debug("details only during development")
logging.info("normal flow events")
logging.warning("something might be wrong")
logging.error("something failed")
Levels (increasing severity): DEBUG < INFO < WARNING < ERROR < CRITICAL.
Why logging over print in production:
| print() | logging |
|---|---|
| Screen only | Console, files, network |
| No levels | DEBUG → CRITICAL levels |
| No timestamps | Timestamps & formats built in |
| No easy disabling | level= filters whole categories |
| One-off | Structured, searchable output |
TDD in One Sentence
Write the test FIRST (it fails), then write the minimum code to make it pass — red, green, refactor.
The loop:
- Red: write a failing test for the behavior you want.
- Green: write the smallest code that makes it pass.
- Refactor: clean up, keeping the tests green.
This forces you to think about behavior before implementation — and guarantees every feature is covered.
Common Mistakes to Avoid
- Mistake: Testing the implementation instead of the behavior — Fix: assert on outputs and effects, not internals.
- Mistake: Only testing the happy path — Fix: test edge cases: zero, negatives, empty inputs, maximums.
- Mistake:
print()debugging in production — Fix: use theloggingmodule. - Mistake: A test that asserts nothing — Fix: every test needs at least one
assert(orpytest.raises). - Mistake: Tests that depend on other tests or on global state — Fix: keep tests independent and deterministic.
Professional Tips & Tricks
- Name tests descriptively:
test_withdraw_insufficient_funds(). - Test edge cases: zero, negatives, empty inputs, and the maximums.
- Use
pytest -vfor verbose output andpytest -k keywordto run a subset. - Use
breakpoint()+n/s/pinstead of print-debugging. - Replace production
print()withlogging— leveled, timestamped, filterable.
Key Takeaways
assertgives quick checks; pytest gives professional test suites.breakpoint()+n/s/pbeats print-debugging.loggingwith levels is the production-grade replacement for print.- TDD: write the failing test first, then make it pass.
- Test behavior, not implementation — and cover edge cases.
Next up: The capstone — build a complete CLI expense tracker.
# A function we want to trust
def price_after_tax(price, tax_rate=0.18):
"""Return price plus tax, rounded to 2 decimals."""
return round(price * (1 + tax_rate), 2)
# Tests using pytest style (run with: pytest)
def test_normal_price():
assert price_after_tax(100) == 118.0
def test_zero_price():
assert price_after_tax(0) == 0.0
def test_high_tax():
assert price_after_tax(100, 0.5) == 150.0
# Logging instead of print
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logging.info("Calculating price for 100")
print("Result:", price_after_tax(100))Lesson Code (Python)
# A function we want to trust
def price_after_tax(price, tax_rate=0.18):
"""Return price plus tax, rounded to 2 decimals."""
return round(price * (1 + tax_rate), 2)
# Tests using pytest style (run with: pytest)
def test_normal_price():
assert price_after_tax(100) == 118.0
def test_zero_price():
assert price_after_tax(0) == 0.0
def test_high_tax():
assert price_after_tax(100, 0.5) == 150.0
# Logging instead of print
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logging.info("Calculating price for 100")
print("Result:", price_after_tax(100))Console Output
INFO: Calculating price for 100
Result: 118.0
# Running pytest:
# ============================ test session starts ============================
# collected 3 items
# test_*.py ..... [100%]
# ============================= 3 passed in 0.02s =============================Code Visualization Tips
- Picture each test as a referee with a checklist — green for pass, red for fail.
- Visualize breakpoint() as a pause button that freezes the program mid-flight so you can inspect it.
- Draw the red-green-refactor loop: fail (red) -> pass (green) -> clean up (refactor).
Professional Tips & Tricks
- Name tests descriptively: test_withdraw_insufficient_funds().
- Test edge cases: zero, negatives, empty inputs, and the maximums.
- Use pytest -v for verbose output and pytest -k keyword to run a subset.
Python Code Judge & Practice Arena
LeetCode StyleRun real Python 3.12 WebAssembly code directly in your browser against automated test suites.
Problem 1: Email Format Validator 1
Test Your Knowledge
Instant feedbackQuick Check: Lesson 24: Unit Testing with Pytest & Unittest, Logging & Debugging
Up next · Continue learning
Capstone — Build a CLI Expense Tracker
Bring everything together: functions, dicts, files, JSON, and a menu loop in one complete project.