Review checklist
This is the checklist to run down — by eye, on any diff, yours or an AI’s — after the tools in Verification Tools have run and come back clean. It fits on one printed page on purpose: a checklist nobody actually opens during a review is worthless. Every line links to the lesson that explains why, and to the exact pattern number in the AI Bug Catalog it guards against.
Mutability
Section titled “Mutability”- Does a default argument use a mutable literal (
= [],= {},= set()) instead of= None? Catalog #1. See Functions. - Does a
lambdaor nested function inside a loop close over the loop variable itself, rather than its value at each iteration? Catalog #2. See Closures. - Does a function that looks read-only actually mutate a
list/dictargument in place (.sort(),.append(),x[i] = ...) without documenting it? Catalog #3. See Lists & Tuples. - Does
[[...] * n] * m(or any*-repeated container of containers) create one shared inner object instead of independent ones? Catalog #4. See Lists & Tuples. - Is a class attribute declared at class level (not inside
__init__) a mutable object shared by every instance? Catalog #15. See Classes.
Errors
Section titled “Errors”- Is
isused anywhere it should be==— comparing values rather than checking a singleton (None/True/False)? Catalog #5. See Variables & Types. - Is there a bare
except:, anexcept Exception: pass, or (in async code) anexcept BaseExceptionsitting between anawaitand a task’s.cancel()? Catalog #6. See Exceptions. - Does
raise eappear insideexcept ... as e:where a bareraisewas intended? Catalog #7. See Exceptions. - Does
assertvalidate external input (arguments, user input, API responses) rather than an internal invariant? It vanishes under-O. Catalog #8. See Exceptions.
Async & concurrency
Section titled “Async & concurrency”- Is every coroutine-returning call actually
awaited, or handed toasyncio.create_task/asyncio.gather? Catalog #9. See asyncio basics. - Does every
asyncio.create_task(...)result get stored somewhere with a lifetime longer than the function that created it? Catalog #10. See Tasks & gather. - Does any
async deffunction call a blocking function (time.sleep, a synchronous HTTP client, blocking file I/O) instead of itsawait-based equivalent? Catalog #11. See GIL & threads. - Is shared mutable state read-then-written across an
awaitpoint with noasyncio.Lock? Catalog #12. See Tasks & gather. - Does every
multiprocessing.Pool/Processcall sit behindif __name__ == "__main__":? Catalog #13. See multiprocessing.
Typing
Section titled “Typing”- Does a class define
__eq__without also defining__hash__(or accepting, on purpose, that instances become unhashable)? Catalog #14. See Dunder methods. - Is
Anyused where a real type was available, or does a# type: ignoreappear with no specific error code? Catalog #16. See Type hints. - Is an
X | None(orOptional[X]) parameter or return value used without aNonecheck before the value is used? Catalog #17. See Type hints. - Is a
list[Subtype]passed wherelist[Supertype]is expected — and wouldSequence/Iterable(covariant) actually be the correct parameter type instead oflist? Catalog #18. See Type hints.
Resources & security
Section titled “Resources & security”- Does the code call
datetime.utcnow()(deprecated, naive) instead ofdatetime.now(timezone.utc)? Catalog #19. See stdlib tour. - Is every
open()call inside awithblock, and does it passencoding=explicitly? Catalog #20. See stdlib tour. - Does any
subprocesscall useshell=Truewith a string built from external input? Catalog #21. See stdlib tour. - Does every
@patch("...")target string match how the code under test actually imported the name — the module that calls it, not the module that defines it? Catalog #22. See pytest. - Is every dependency pinned, and has
pip-audit/uv’s audit tooling run against the lockfile recently? Is anything ever installed withsudo pip? Catalog #23. See venv, pip & uv.
Tooling & style
Section titled “Tooling & style”- Does library code call
print()whereloggingbelongs — anything the caller needs to be able to filter, redirect, or silence? Catalog #24. See Logging.
🐛 Spot the bug
def get_config(overrides: dict | None = None) -> dict: config = {"retries": 3, "timeout": 10} if overrides: config |= overrides return config
base = get_config()custom = get_config({"retries": 5})print(base)print(custom)Show the answer
This one is actually correct — it is here to test whether you run the checklist mechanically or actually read the code. overrides: dict | None = None uses None as the default, not a mutable literal, so catalog #1 does not apply: config = {...} is a fresh dict created inside the function body on every call, and config |= overrides merges into that fresh dict, not into anything shared across calls. Running the checklist should tell you where to look, not hand you a bug that isn’t there — base correctly prints {'retries': 3, 'timeout': 10} and custom prints {'retries': 5, 'timeout': 10}, independently.
Copy-paste short version
Section titled “Copy-paste short version”Mutability: mutable default arg? loop-var closure? mutates caller's list? shared [[...]]*n rows? shared class attr?Errors: is vs ==? bare/broad except (incl. BaseException across await)? raise e vs raise? assert validating input?Async: every coroutine awaited/tasked? task reference kept? blocking call inside async def? shared state without Lock? Pool guarded by __main__?Typing: __eq__ without __hash__? bare Any / uncoded type: ignore? Optional narrowed before use? list invariance respected?Resources: utcnow()/naive datetime? open() with encoding=? subprocess shell=True? patch target matches import site? deps pinned + audited?Tooling: print() instead of logging in library code?