Skip to content

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.

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.

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 bare except:) 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 assert appear where real input validation belongs? assert vanishes entirely under python -O.
  • In async code, is except Exception (or a broader except 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.

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 return or a raised exception — usually via with, not a bare .close() at the end of the function?
  • For an asyncio.create_task(...) call: is the returned Task object stored anywhere? An unreferenced task can be garbage-collected before it finishes, silently dropping whatever work it was doing.
  • Does anything ever await that task, so a raised exception inside it actually surfaces instead of vanishing?

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 None where the hint says str, because nothing enforces it at the call the code was tested with?
  • Is Any used anywhere a more specific type was available? Any does 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)
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 Python you did not write
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.
What you getBenefitCost
Tracing mutable data flow firstCatches aliasing and shared-default bugs before you trust any return valueRequires knowing which builtins are mutable and which argument-passing actually shares state
Tracing the whole exception pathFinds swallowed, mis-scoped, or assert-based “validation” that AI code hidesSlower than reading only the happy-path return
Tracking task/resource lifetimes explicitlyFinds leaked file handles and dropped background tasks before they page someone at 3amRequires holding more state in your head while reading async code
Treating type hints as claims, not guaranteesFinds the gap between what a signature promises and what the body actually doesRequires running mypy/pyright yourself — the hint alone proves nothing
  • “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 return is reading the function.” The except clauses, the asserts, and the code between an await and 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.
Before reading anything else about this function, what have you already learned from its body?
def restock(inventory: dict[str, int], item: str, qty: int) -> dict[str, int]:
    inventory[item] = inventory.get(item, 0) + qty
    return inventory
Why is the exception path usually more important to read carefully than the happy-path return?
For asyncio.create_task(send_metric(...)) called with no variable to hold the result, what should a reviewer check?
A function is annotated def greeting(name: str) -> str:. What does that annotation actually guarantee at runtime?
Why does this module's reading order put 'follow mutable data flow' before 'read type hints'?