Python Atlas
Software engineering

Debugging and profiling

Reproduce failures, inspect state with pdb, and profile before changing performance-sensitive code.

Debugging explains incorrect behavior. Profiling explains where resources are spent. Both should begin with evidence rather than intuition.

A reproducible debugging loop

  1. Record the exact command, input, environment, and observed output.
  2. Reduce the failure to the smallest reliable reproduction.
  3. State one falsifiable hypothesis.
  4. Add an observation: a focused test, log, assertion, or breakpoint.
  5. Change one variable.
  6. Preserve the reproduction as a regression test.

Read the complete traceback from the final exception upward. Earlier frames show how execution arrived there; chained exceptions often contain the original cause.

Use assertions for internal invariants

def average(total: float, count: int) -> float:
    if count <= 0:
        raise ValueError("count must be positive")
    result = total / count
    assert result >= 0, "average must be non-negative for validated input"
    return result

Validate user input with normal exceptions. Assertions can be disabled with optimization and must not enforce security or public API requirements.

Debug with pdb

Insert a temporary breakpoint:

def reconcile(expected: int, actual: int) -> int:
    difference = actual - expected
    breakpoint()
    return difference

Useful pdb commands:

  • l or list: show nearby source;
  • p expression: evaluate and display an expression;
  • pp expression: pretty-print a value;
  • n or next: run the next line in the current frame;
  • s or step: enter a called function;
  • r or return: continue until the current function returns;
  • u / d: move up or down the call stack;
  • where: display the stack;
  • c or continue: continue execution;
  • q or quit: stop debugging.

You can enter post-mortem debugging after an unhandled exception:

python -m pdb -c continue -m inventory.cli

For tests, many runners support dropping into the debugger on failure. Remove committed breakpoint() calls unless they are deliberately guarded development tooling.

Avoid accidental debugger changes

Evaluating expressions can call properties, mutate state, or consume generators. Inspect simple values first. A debugger also changes timing, so it may hide races and timeouts. For concurrency bugs, add timestamps, task or thread identity, and correlation context.

Measure wall-clock behavior

Use timeit for small, isolated operations:

from timeit import timeit

duration = timeit(
    "sorted(values)",
    setup="values = list(range(10_000, 0, -1))",
    number=100,
)
print(f"{duration:.3f} seconds")

Keep setup outside the timed statement unless setup is part of the behavior. Run enough repetitions, compare distributions, and use realistic input shapes.

Profile CPU use

Use cProfile to find cumulative cost:

python -m cProfile -o profile.prof -m inventory.cli import records.csv
python -m pstats profile.prof

Inside pstats, try sort cumulative and stats 20. Cumulative time includes callees and is useful for finding expensive call paths; internal time identifies work in the function itself.

Programmatic profiling is useful around a specific operation:

import cProfile

with cProfile.Profile() as profiler:
    run_import()

profiler.dump_stats("profile.prof")

Profile production-like data in an appropriate environment. Development mode, warm caches, network variability, and tracing overhead can distort results.

Memory and I/O

tracemalloc compares Python allocation snapshots:

import tracemalloc

tracemalloc.start()
before = tracemalloc.take_snapshot()
run_import()
after = tracemalloc.take_snapshot()

for statistic in after.compare_to(before, "lineno")[:10]:
    print(statistic)

It does not account for every allocation in native extensions. For I/O-bound code, collect request latency, retry counts, queue time, and bytes transferred; CPU profiling alone may point at waiting wrappers rather than the external bottleneck.

Pitfalls

  • changing code before reproducing the failure;
  • adding broad exception handling that hides the symptom;
  • trusting one benchmark run;
  • profiling tiny synthetic inputs that exercise a different path;
  • optimizing a function with low cumulative impact;
  • confusing correlation with cause in logs;
  • leaving debug output or sensitive snapshots in source control.

Checklist

  • The failure has a deterministic or well-characterized reproduction.
  • A hypothesis predicts a specific observation.
  • The fix has a regression test.
  • Benchmarks isolate the intended operation.
  • Profiling uses representative data and environment.
  • CPU, memory, and I/O are considered separately.
  • Performance improvements preserve readability and correctness.

Practice

Create a deliberately slow report that repeatedly searches a list. Capture a CPU profile, identify the cumulative hotspot, replace repeated scans with an index, and profile again. Record input size, command, elapsed time, and memory tradeoff so the result is reproducible.

LEARNING RECORD

Finish this lesson

Mark it complete when you can explain the main decision without looking.

KNOWLEDGE CHECK

Debugging and profiling

1After reducing a bug to a reliable reproduction, what is the most disciplined next step?
2In cProfile results, why is cumulative time useful?