How to read Python you did not write
Every earlier module in this course taught you to write Python. This module teaches the other half of the job: reading Python someone — or something — else wrote, and deciding whether it is correct. An AI assistant will hand you code that runs, prints plausible output, and looks exactly like the idiomatic examples you have been studying. Running without error is not the same as correct. The four questions below are the order experienced Python reviewers actually read code in, and every later lesson in this module drills one of them.
Follow where mutable data flows
Section titled “Follow where mutable data flows”Python passes every argument by object reference, and most of the objects you pass around — list, dict, set, and any class instance without frozen=True — are mutable. Before you read what a function returns, read what it touches:
- Does the function mutate an argument in place, return a new object, or both?
- If it returns the same object it was given (
return the_list), does the caller expect a new list, or does it expect the mutation and the return value to be the same thing? - Is a default argument (
def f(items=[])) a mutable object that gets created once, at definition time, and then shared across every call that does not pass its own? - Does a class attribute declared at the class level (not inside
__init__) get shared across every instance, the way a module-level variable is shared across every caller?
None of this requires running the code. A reviewer who reads def restock(inventory: dict[str, int], item: str, qty: int) -> dict[str, int]: and sees the body does inventory[item] = ... before return inventory already knows: this mutates the caller’s dict and hands back the same object — so the next thing to check is whether every caller expected that.
Follow the exception path
Section titled “Follow the exception path”Python’s try/except is opt-in, not enforced by the compiler the way a Go or Rust error return is — nothing forces a caller to notice that a function can raise. Reading a function well means tracing every way it can fail, not just its happy-path return:
- What does the function raise, and under what condition? Is that documented anywhere, or only discoverable by reading the body?
- Is a broad
except Exception(or bareexcept:) used anywhere between the raise and where you’d expect it to be handled — and does it swallow more than the one failure mode it was written for? - Does
assertappear where real input validation belongs?assertvanishes entirely underpython -O. - In async code, is
except Exception(or a broaderexcept BaseException) sitting between a task and its.cancel()— swallowing the cancellation itself?
The lesson that catalogs exactly how AI-generated code gets this wrong is The AI Bug Catalog — this page assumes you already know try/except/finally from the Exceptions lesson, and instead trains you to spot the violations.
Follow task and resource lifetimes
Section titled “Follow task and resource lifetimes”Anything with a .close(), a with block, or a create_task() call has a lifetime that Python does not track for you. For every acquired resource or spawned task, ask:
- Who is responsible for closing it — the function that opened it, or whatever it was handed to?
- Is the release guaranteed on every path, including an early
returnor a raised exception — usually viawith, not a bare.close()at the end of the function? - For an
asyncio.create_task(...)call: is the returnedTaskobject stored anywhere? An unreferenced task can be garbage-collected before it finishes, silently dropping whatever work it was doing. - Does anything ever
awaitthat task, so a raised exception inside it actually surfaces instead of vanishing?
Read type hints skeptically
Section titled “Read type hints skeptically”A type hint is a claim, not a guarantee. Nothing checks it at runtime unless you add your own validation:
- Does the function’s body actually match what the signature promises — or does it silently accept
Nonewhere the hint saysstr, because nothing enforces it at the call the code was tested with? - Is
Anyused anywhere a more specific type was available?Anydoes not just mean “unchecked here” — it turns off checking for everything it touches downstream, too. - Was the type hint checked with a real tool (
mypy --strict,pyright) — or does it just look plausible because it compiles (there is no such thing as a Python hint failing to “compile”)?
def restock(inventory: dict[str, int], item: str, qty: int) -> dict[str, int]: # Reading order in practice: (1) this function MUTATES the dict you pass # in -- it does not copy it -- so every caller's reference to that same # dict sees the change too. (2) It can raise ValueError; the return value # on the happy path is the SAME object as the input, which is easy to # miss if you only skim the final return statement. if qty < 0: raise ValueError(f"qty must be >= 0, got {qty}") inventory[item] = inventory.get(item, 0) + qty return inventory
warehouse = {"widgets": 10}same_dict = restock(warehouse, "widgets", 5)print("warehouse after restock:", warehouse)print("same object returned:", same_dict is warehouse)
try: restock(warehouse, "widgets", -3)except ValueError as e: print(f"restock rejected: {e}")print("warehouse unchanged after the rejected call:", warehouse)Loading Python runtime (first run only)…
Mental Model
Section titled “Mental Model”flowchart TD A["Read the code"] --> B["Where does mutable state flow -- mutated in place, or copied?"] B --> C["What can raise here, and who actually catches it?"] C --> D["What owns each task/resource, and who releases it?"] D --> E["Do the type hints hold at runtime, or are they just claims?"] E --> F["Only now: judge correctness"]
Reading order for a function you did not write:
1. Mutable data flow → mutated in place? returned as a new object? both? shared class/default state?2. Exception path → what can raise, is it documented, is it swallowed by something too broad?3. Task/resource life → who closes it, is release guaranteed on every path, is a task ever awaited?4. Type hints → checked by mypy/pyright, or just plausible-looking prose?
Skipping straight to "it ran and printed something reasonable" answers none of these.Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
| Tracing mutable data flow first | Catches aliasing and shared-default bugs before you trust any return value | Requires knowing which builtins are mutable and which argument-passing actually shares state |
| Tracing the whole exception path | Finds swallowed, mis-scoped, or assert-based “validation” that AI code hides | Slower than reading only the happy-path return |
| Tracking task/resource lifetimes explicitly | Finds leaked file handles and dropped background tasks before they page someone at 3am | Requires holding more state in your head while reading async code |
| Treating type hints as claims, not guarantees | Finds the gap between what a signature promises and what the body actually does | Requires running mypy/pyright yourself — the hint alone proves nothing |
Common Misconceptions
Section titled “Common Misconceptions”- “It ran and printed something reasonable, so it’s correct.” Every pattern in this module’s catalog runs and produces plausible output on its happy path — that is exactly what makes it dangerous.
- “Reading the happy-path
returnis reading the function.” Theexceptclauses, theasserts, and the code between anawaitand the next line are usually where AI-generated code hides its mistakes. - “If a human wrote it, it’s fine; only AI code needs this scrutiny.” Every pattern in this module’s catalog also appears in human-written Python — AI just produces it at a much higher and more confident-looking volume.
- “A type hint means the value is guaranteed to have that type.” A hint is unchecked at runtime unless something (a validator, a checker in CI) actually enforces it. Untrusted input can violate any hint silently.