Course outline

Python / Reliable programs / Lesson 8

Errors, exceptions, and debugging

Turn failures into precise information while preserving their causes, then investigate the smallest reproducible failing case.

About 5 min readContent reviewed
On this page

Lesson goals

  • Read the traceback as evidence
  • Log useful context, protect data

Read the traceback as evidence

A syntax error prevents valid source parsing, while an exception interrupts execution at runtime. A traceback shows the call chain that led to the failure, ending with an exception type and message. Read the last operation first, then work upward to understand which caller supplied the problematic state. The final line is a starting point rather than the whole diagnosis: a failing arithmetic operation may expose an earlier invalid conversion. Preserve exact error messages in bug reports, along with minimal input and interpreter version. Reproducing the same failure is more useful than immediately changing several unrelated lines in the hope that it disappears.

Catch what you can resolve

A try block should cover the smallest operation whose expected failure you can handle. Catch specific exception types and either recover under a documented rule or add context and re-raise. A broad except Exception can conceal programming defects when it substitutes a normal-looking result. The else block runs when the try body succeeds; finally runs during exit and is appropriate for cleanup that cannot be expressed with a context manager. Do not return from finally casually because it can suppress an exception or replace a pending result. Unexpected failures should remain visible at a boundary that can report them responsibly.

Translate errors without losing causes

Low-level conversion errors do not always speak the vocabulary of the caller. A domain-specific exception can explain which field failed while chaining the original exception with raise ... from error. The chain preserves useful implementation evidence for debugging. In the example, parsing the quantity happens in its own narrow try block, and range validation happens afterward. Negative integers and invalid text therefore receive distinct explanations. The demonstration catches its own expected domain error only to show a deterministic message; a real application would decide whether to ask for correction, reject a request, or terminate at its outer boundary.

python Translate errors without losing causes β€” worked example
class QuantityError(ValueError):
    pass

def parse_quantity(text):
    try:
        quantity = int(text)
    except ValueError as error:
        raise QuantityError("quantity must be an integer") from error
    if quantity < 0:
        raise QuantityError("quantity must be nonnegative")
    return quantity

try:
    parse_quantity("many")
except QuantityError as error:
    print(error)
    print(type(error.__cause__).__name__)
Expected output
quantity must be an integer
ValueError

The domain error adds field meaning while __cause__ retains the original conversion error. The printed class name verifies chaining without platform-dependent traceback lines.

Inspect assumptions step by step

A debugger allows execution to pause so you can inspect local variables and move through statements. The built-in breakpoint function enters the configured debugger; pdb is available in the standard library. Use debugging in a controlled local process, since stopping a production worker changes its availability. Start with an explicit hypothesis: perhaps a subtotal is overwritten during the second iteration. Inspect that variable at the relevant point rather than stepping through every imported module. The second example uses a simple trace table printed from a loop, which offers the same discipline without interactive prompts and is safe to run as a complete demonstration.

python Inspect assumptions step by step β€” worked example
total = 0
for position, value in enumerate([2, 5], start=1):
    before = total
    total += value
    print(position, before, value, total)
Expected output
1 0 2 2
2 2 5 7

Each row records position, old total, input, and new total, so an incorrect reset would be visible at the first divergent iteration.

Log useful context, protect data

Logging records events for later investigation and differs from returning application results. Choose levels according to operational meaning, include identifiers that connect related events, and avoid secrets or complete sensitive payloads. logging.exception inside an exception handler records traceback information. Do not log the same failure at every layer if that produces duplicate noise. A strong debugging loop narrows input, states an expected invariant, observes the actual state, changes one relevant cause, and repeats the failing case. Once repaired, preserve a regression test. That test should describe the external behavior that broke rather than permanently reproducing every temporary debug print.

Common errors and fixes

An invalid file silently becomes an empty report

Why it happens
A broad handler returns empty data for every exception.
How to fix it
Catch expected failures narrowly and report unrecoverable cases.
Prevent it next time
Test malformed input separately from genuinely empty input.

The original failure vanishes from the diagnosis

Why it happens
Error translation discards the causal relationship.
How to fix it
Use explicit exception chaining when translating abstraction levels.
Prevent it next time
Inspect both domain message and cause in targeted tests.

Debugging exposes credentials in logs

Why it happens
Complete input objects are logged without considering their contents.
How to fix it
Log only necessary safe fields and redact sensitive values.
Prevent it next time
Design log fields with the same care as public error responses.

Check your understanding

1. How should a numeric parser distinguish invalid syntax from a disallowed negative value?

Show worked solution

Place int(text) alone in a try that catches ValueError, translating it to a syntax-specific domain message with a cause. After conversion, check quantity < 0 and raise a range-specific error. Keeping validation outside the try prevents a deliberately raised domain error from being mistaken for a conversion failure.

2. Create a debugging plan for a total that becomes wrong only on the second iteration.

Show worked solution

Use the smallest two-item input that reproduces the failure. Record the old total, current value, and new total each iteration. Compare against the sum-of-prefix invariant. Find the first divergence, repair that update or initialization, and add a two-item regression case alongside empty and one-item cases.

Key takeaways

  • Catch an exception only where a meaningful response exists.
  • Preserve causes when translating errors.
  • Reproduce, inspect, repair, and retain a regression case.

Official references