Debugging, Profiling, and Linting
What You'll Learn
Practical debugging techniques, how to profile slow Python code, and how to use linting tools to catch bugs before they reach production.
Debugging with print() — The First Tool
The simplest debugging: print what you don't understand:
def calculate_total(prices: list[float], discount: float) -> float:
print(f"DEBUG prices={prices} discount={discount}") # ← add this
subtotal = sum(prices)
print(f"DEBUG subtotal={subtotal}")
result = subtotal * (1 - discount)
print(f"DEBUG result={result}")
return result
Remove debug prints once fixed. Better: use log.debug() which you can silence without removing.
breakpoint() — Built-In Debugger (Python 3.7+)
breakpoint() drops you into an interactive debugger at that line:
def process_order(order: dict) -> float:
items = order["items"]
breakpoint() # execution stops here — inspect state interactively
total = sum(item["price"] * item["qty"] for item in items)
return total
pdb Commands
(Pdb) n # next line (step over)
(Pdb) s # step into function call
(Pdb) c # continue until next breakpoint
(Pdb) q # quit debugger
(Pdb) p variable # print variable value
(Pdb) pp variable # pretty print
(Pdb) l # list surrounding code
(Pdb) ll # list entire current function
(Pdb) where # show call stack
(Pdb) b 42 # set breakpoint at line 42
(Pdb) u # move up in call stack
(Pdb) d # move down in call stack
Conditional Breakpoints
for i, item in enumerate(items):
if i == 500: # only break on the 501st item
breakpoint()
process(item)
VS Code Debugger (GUI)
For a visual debugger, use VS Code with the Python extension:
- Click the gutter (left of line number) to set a breakpoint (red dot)
- Press
F5to start debugging - Use the debug toolbar: Step Over, Step Into, Continue, Stop
This is often faster than command-line pdb for complex bugs.
Common Debugging Patterns
Inspect intermediate values
# Replace a complex one-liner with steps
result = [process(x) for x in items if x.is_valid() and x.score > 0.5]
# Debug version
valid = [x for x in items if x.is_valid()]
print(f"valid: {len(valid)}")
high_score = [x for x in valid if x.score > 0.5]
print(f"high_score: {len(high_score)}")
result = [process(x) for x in high_score]
print(f"result: {len(result)}")
Narrow down the failing case
# Which item causes the crash?
for i, item in enumerate(items):
print(f"Processing item {i}: {item}")
process(item) # crash somewhere in here
Check types when confused
print(type(value), repr(value))
Profiling — Finding Slow Code
Don't optimize without measuring first — you'll optimize the wrong thing.
cProfile — Built-In Profiler
# Profile a script
python3 -m cProfile -s cumulative slow_script.py
# Profile with output to file
python3 -m cProfile -o profile.out slow_script.py
# Profile a function inline
import cProfile
with cProfile.Profile() as pr:
result = slow_function(data)
pr.print_stats(sort="cumulative", limit=20)
Output:
ncalls tottime percall cumtime percall filename:lineno(function)
1000 0.234 0.000 1.230 0.001 processor.py:42(process_item)
500 0.987 0.002 0.987 0.002 database.py:18(query)
Look at cumtime (total time including called functions). The biggest numbers are your bottlenecks.
timeit — Measure Small Snippets
import timeit
# Compare two approaches
t1 = timeit.timeit('"+".join(str(n) for n in range(100))', number=10000)
t2 = timeit.timeit('"+".join([str(n) for n in range(100)])', number=10000)
print(f"Generator: {t1:.3f}s, List: {t2:.3f}s")
python3 -m timeit -n 10000 '"+".join(str(n) for n in range(100))'
line_profiler — Profile Line by Line
pip install line_profiler
from line_profiler import profile
@profile
def slow_function(data):
result = []
for item in data:
processed = transform(item) # ← which line is slow?
result.append(processed)
return result
kernprof -l -v script.py
Linting — Catch Bugs Before Runtime
ruff — Fast Linter + Formatter
pip install ruff
ruff check src/ # lint
ruff check src/ --fix # auto-fix what it can
ruff format src/ # format (like black)
Common issues ruff catches:
- Unused imports
- Undefined variables
- Shadowed built-ins
- Style violations
- Dangerous patterns
mypy — Type Checker
pip install mypy
mypy src/
def greet(name: str) -> str:
return "Hello, " + name
greet(42) # mypy: error: Argument 1 to "greet" has incompatible type "int"
Setting Up in pyproject.toml
[tool.ruff]
line-length = 99
target-version = "py311"
select = ["E", "F", "I", "N", "W"]
[tool.mypy]
python_version = "3.11"
strict = false
ignore_missing_imports = true
Quick Debugging Workflow
1. Reproduce the bug consistently
↓
2. Add print() or breakpoint() near the failure
↓
3. Narrow down: which line, which variable, which value
↓
4. Fix the root cause (not the symptom)
↓
5. Write a test that would have caught it
↓
6. Remove debug prints
Quick Reference
# Print debug
print(f"DEBUG: {var=}") # Python 3.8+ — prints "var=value"
# Breakpoint
breakpoint() # drops into pdb
# pdb commands
# n=next s=step c=continue q=quit p=print l=list
# Profile
import cProfile
cProfile.run("function()", sort="cumulative")
# Timeit
import timeit
timeit.timeit("expression", number=10000)
# Type check on the fly
type(x)
isinstance(x, str)
# CLI tools
ruff check src/
ruff format src/
mypy src/
python3 -m cProfile -s cumulative script.py
python3 -m timeit "expression"