androidengineers.Book a session

Python 06 · Errors and resource management

Debugging, logging, and reproducible failures

articleSelf-paced

Learn the concept

Debugging begins with a reproducible case. Reduce the input until the failure is understandable, inspect intermediate values, and verify one hypothesis at a time. Random edits can hide the problem without explaining it.

Logging records events during normal operation. Use levels to distinguish routine information from actionable failures. Include a request or operation ID so related events can be connected, but avoid logging credentials or unnecessary private input. A useful error log identifies the failed stage and outcome.

A debugger lets you pause at a breakpoint and inspect state. Assertions are useful in tests and internal invariants; do not rely on them for validating untrusted inputs because optimized execution can remove them. Keep a failing test after the bug is fixed.

Run and inspect

import logging

logger = logging.getLogger("document_pipeline")
def count_valid(records):
    count = sum(1 for record in records if record.get("active") is True)
    logger.info("batch_processed count=%s", count)
    return count

assert count_valid([{"active": True}, {"active": False}]) == 1

Your exercise

Introduce an off-by-one bug in a batch counter. Create the smallest failing test, inspect it with a breakpoint, fix it, and preserve the regression test.

Check your understanding

You can explain the cause, show the failing and passing test, and confirm no sensitive payload is written to logs.

Guided workshop: investigate one failing record

A useful bug report includes a minimal input, expected result, observed result, and a reproducible command. Start with the last exception in the traceback, then follow the application frames to the value that violated an assumption. Changing several unrelated lines at once makes it harder to discover the cause.

# academy-check
import json

def parse_line(line, number):
    try:
        value = json.loads(line)
    except json.JSONDecodeError:
        return {"line": number, "status": "rejected", "reason": "invalid_json"}
    if not isinstance(value, dict) or not isinstance(value.get("text"), str):
        return {"line": number, "status": "rejected", "reason": "missing_text"}
    return {"line": number, "status": "accepted", "characters": len(value["text"])}

assert parse_line('{"text":"hello"}', 1)["characters"] == 5
assert parse_line('{bad', 2)["reason"] == "invalid_json"
assert parse_line('[]', 3)["reason"] == "missing_text"

These outcomes preserve a location and reason without logging the full document. A line number is meaningful only together with an input identifier, so the outer application should also record a source file or import ID. A stack trace is useful to an operator but usually unsuitable as the user-facing response.

Exercise: introduce a defect by removing the dictionary check. Re-run the list input and read the resulting error. Restore the check and add a regression assertion. Then use a temporary directory and a with open(...) block to read three lines and collect the results. Verify that the file closes when processing raises.

Worked diagnosis: JSON decoding succeeded for []; the failure occurred because the code assumed every valid JSON value was an object. Catching only decoding errors could never handle this defect. Structural validation must follow parsing.

Checkpoint: distinguish a recoverable bad record from an unreadable input file. Continuing after the former can produce an honest rejected-record count. Continuing after the latter while reporting success would conceal that the source was never processed. Record failures explicitly and propagate unexpected programming errors.

YOUR LEARNING JOURNEY

0 of 42 available lessons completed

Progress saved in this browser. No account needed.
Debugging, logging, and reproducible failures | Python for AI Engineering | Android Engineers