Skip to content
v0.1.0latest release21 skills2 categories3 sub-categories22 tested scripts68 reference docsContact us

Review Checklist

Reference for python-idiomatic-code-reviewer · 289 lines

Programming / Pythonv1.0.0Updated OCT 20 20265 references1 script

How to run a Python idiom review end to end, grade severity consistently, avoid false positives, and write comments that get acted on. Pairs with scripts/review_idioms.py.

Contents

  1. Before you start
  2. Running the scanner
  3. Manual pass checklist
  4. Grading severity
  5. Rule reference and false positives
  6. Writing review comments
  7. Rewriting safely
  8. Reporting
  9. CI integration

Before you start

Answer these before judging any line:

QuestionWhy it changes the review
Minimum Python version?Decides whether match, X | Y, TaskGroup, batched, StrEnum are allowed
Library, service, CLI, notebook or one-off script?print is fine in CLIs/notebooks, not in libraries; module-level clients matter more in services
Sync or async?Blocking-call rules only matter in async code
Hot path?Micro-idioms (join vs +=, set membership) matter in loops over corpora, not in config loading
Existing style tools? (ruff, black, mypy)Do not duplicate what the formatter already enforces; focus on semantics
Tests present?Determines how aggressive the rewrite can be

Running the scanner

bash
python scripts/review_idioms.py path/to/file.py                 # full review with rewrites
python scripts/review_idioms.py src/ --min-severity medium      # triage a whole package
python scripts/review_idioms.py src/ --json > review.json       # feed into another tool
python scripts/review_idioms.py src/ --select PY036,PY037,PY038,PY039,PY040,PY041   # production-risk sweep
git diff --name-only main -- '*.py' | xargs python scripts/review_idioms.py   # only changed files

JSON shape:

json
{
  "files": 1,
  "summary": {"high": 1, "medium": 0, "low": 2},
  "findings": [
    {"path": "app.py", "line": 14, "col": 1, "rule": "PY036", "name": "hardcoded-secret",
     "severity": "high", "message": "...", "source": "OPENAI_API_KEY = \"sk-...\"", "fix": "..."}
  ],
  "errors": []
}

The scanner is a first pass. Treat each finding as a question ("is this really a problem here?"), not a verdict.

Manual pass checklist

Go through the code once more with this list; most items cannot be detected statically.

Correctness

  • Every rewrite candidate is behaviour-equivalent (see Rewriting safely)
  • Functions return one type; no None-or-list mixes; errors raise, not return strings
  • No shared mutable state: module-level lists/dicts mutated by functions, class attributes used as instance state
  • Float comparisons use tolerances (math.isclose, np.allclose)
  • Integer division and off-by-one in slicing, chunk overlap, pagination
  • dict.get(k, default) vs stored None values
  • Iterators consumed twice (generators, zip, map, file handles)
  • Late-binding closures in loops (lambda: i captures the variable, not the value)

Errors

  • No bare except:; broad except Exception only at boundaries (request handler, worker loop) and always logged
  • raise ... from err when translating exceptions
  • try blocks contain only the call that can fail
  • Cleanup in finally or a context manager, never only on the happy path

Resources

  • Files, sockets, sessions, DB connections, locks use with / async with
  • Text I/O passes encoding="utf-8"
  • Clients are created once and reused; closed on shutdown

Async and concurrency

  • No blocking calls in async def (including sync SDK methods and CPU-heavy work)
  • Every coroutine is awaited; created tasks are kept or owned by a TaskGroup
  • Concurrency is bounded (Semaphore) and matched to rate limits
  • Timeouts on every network call and an overall deadline per request

AI specifics

  • No secrets in code, notebooks, test fixtures or default args
  • Retries bounded, only on retryable errors, with jittered backoff; not stacked on top of SDK retries unknowingly
  • Model output is parsed and validated (pydantic / JSON schema), never evaled
  • Prompts are templates with named fields; untrusted text is delimited
  • Token counting uses the tokenizer, not len(str)
  • Prompts/completions/PII not logged at INFO
  • numpy/torch truthiness not used on arrays/tensors

Readability

  • Names say what, not how (relevant_chunks, not lst2)
  • Functions do one thing; flag arguments split into two functions
  • Comments explain why; no commented-out code
  • Type hints on public functions

Grading severity

SeverityTestTypical rulesAction
highCan this cause wrong output, data loss, a security incident or an outage in normal operation?PY001–PY005, PY036–PY038, PY040Must fix before merge
mediumCorrect today but breaks on a plausible edge case, input type or load patternPY006, PY008–PY010, PY012, PY039, PY041Fix before merge unless justified
lowReadability, idiom, consistency, minor perfPY007, PY011, PY013–PY035, PY042–PY046Suggest; batch into one comment

Adjust by context:

  • Raise by one level when the code is on a hot path, handles user input, or runs unattended (workers, cron jobs).
  • Lower by one level in throwaway scripts, notebooks, or tests (except secrets: always high).
  • A low finding repeated 50 times is still low; mention the pattern once with a count.

Rule reference and false positives

RuleNameCommon false positive / exceptionWhat to do
PY001mutable-defaultSentinel used only for identity, never mutatedStill prefer None; or use a module-level _MISSING = object()
PY002bare-exceptTop-level crash reporter that re-raisesUse except BaseException: explicitly and re-raise
PY003swallowed-exceptionBest-effort telemetry/cleanupLog at debug, or contextlib.suppress(SpecificError)
PY004eval-execSandboxed code-execution tool (agent code interpreter)Must run in a separate process/container with limits; document it
PY005shell-injectionConstant command string with no interpolationStill prefer a list; acceptable with a # noqa: PY005 and comment
PY006compare-nonenumpy/pandas element-wise == None intendedUse pd.isna / np.equal(x, None) explicitly
PY007compare-boolTri-state values where None must differ from FalseUse is True / is False
PY008type-compareDeliberate exact-type check (exclude bool from int)type(x) is int with a comment
PY009open-without-withHandle returned to the caller who closes itDocument ownership; return a context manager instead if possible
PY010star-importPackage __init__ re-export with __all__Acceptable with __all__ defined
PY011raise-without-fromIntentionally hiding the causefrom None makes it explicit
PY012range-lenLoop that mutates xs[i] in placefor i, x in enumerate(xs): xs[i] = f(x) or a new list
PY013manual-counterCounter incremented conditionallyNot a counter; ignore
PY014dict-keysIterating while deleting keysfor k in list(d): is the idiom
PY015append-loopLoop body with side effects (logging, I/O)Keep the loop
PY016any-all-loopLoop does work before returningKeep the loop
PY017setdefaultExpensive default constructordefaultdict(factory) still fine
PY018str-concat-loopFew iterations, readability winsLow; leave in small loops
PY019old-formatTemplates filled later (TEMPLATE.format(...)), logging, SQL paramsTemplates with .format on a constant are fine
PY020len-truthinessnumpy arrays, DataFrames, tensorsKeep len() / .size / .empty
PY021not-in—Always fix
PY022isinstance-chain—Always fix
PY023lambda-assign—Always fix
PY024shadow-builtinPydantic/dataclass fields named id, type (class level is not flagged)Parameters mirroring an external API (input=) can stay
PY025os-pathos.path in hot loops over millions of strings (slightly faster)Measure first
PY026open-encodingBinary data, or locale encoding genuinely intendedencoding="locale" (3.10+) makes intent explicit
PY027utcnowInterop with a library expecting naive UTCConvert at the boundary
PY028global-stmtTiny scriptsLow
PY029elif-chainConditions that are not simple equalityNot flagged; fine
PY030lbyl-existsExists check that chooses between two code pathsFine if no race matters
PY031typing-genericsMust support 3.8Keep until 3.9 is the floor
PY032semicolon—Always fix
PY033backslashwith statements before 3.10Allowed for multi-item with below 3.10
PY034print-debugCLI output, notebooks, functions whose job is printingprint*/show*/report*/main are exempt; others # noqa
PY035logging-formatMessage built once at startupLow
PY036hardcoded-secretTest fixtures with fake keysUse obviously fake values ("test-key") or env in tests
PY037sleep-in-asyncCode in a thread started via to_thread but defined asyncNot possible; always fix
PY038blocking-in-asyncStartup code in an async lifespan that runs onceLow impact; still prefer async
PY039http-no-timeoutSession with a mounted adapter enforcing timeoutsDocument it
PY040unbounded-retryWorker main loop that polls a queue (not a retry)Fine if errors are logged and not tight-looping
PY041import-time-clientTiny scripts, notebooksFine there
PY042llm-client-no-timeoutTimeout set via with_options per callFine
PY043prompt-concatTwo-piece concatenation in a tiny helperLow
PY044sequential-awaitCalls that depend on the previous result, or strict ordering/rate limitsKeep sequential; say why
PY045suppress—Fix
PY046walrusTarget codebase below 3.8, or team prefers explicit linesOptional

Suppress a reviewed exception inline: do_thing() # noqa: PY034 - CLI output.

Writing review comments

A good comment has four parts: where, what, why it matters here, concrete fix.

text
L14 [high] PY036 hardcoded key
The OpenAI key is committed in source; anyone with repo access (and any bot scanning
GitHub) can use it. Please revoke it and read it from the environment:

    api_key = os.environ["OPENAI_API_KEY"]
text
L30 [high] PY037 time.sleep inside async def
fetch_context() runs on the event loop; time.sleep(1) pauses every request in this
worker for a second. Use `await asyncio.sleep(1)`.
text
L24, L41, L57 [low] PY012 range(len(...)) (3 places)
Iterating directly reads better and avoids index bugs:

    for text in texts: ...
    for i, text in enumerate(texts): ...   # if you need i

Tone and form:

  • Lead with the impact, not the rule name.
  • Be specific about the fix; include the code.
  • Group repeated low findings into one comment.
  • Mark optional suggestions as "nit:" or "optional:"; do not block a merge on them.
  • Ask a question when intent is unclear ("Is swallowing this deliberate? If so, a comment would help").
  • Praise good patterns briefly when they are non-obvious (a correct TaskGroup, a clean retry policy).

Rewriting safely

Equivalence traps to check before suggesting a rewrite:

RewriteTrap
loop → comprehensionLoop variable no longer exists after a comprehension; side effects in the loop body are lost
loop → any()/all()Generator stops at the first hit, same as the loop; but all([]) is True
len(x) == 0 → not xBreaks for numpy arrays/DataFrames; changes meaning if x can be None
== None → is NoneChanges behaviour for objects with custom __eq__ (intended)
range(len(a)) → zip(a, b)zip truncates silently; use strict=True (3.10+)
except Exception → specific errorsErrors that were swallowed now propagate; call it out
%/.format → f-stringf-strings evaluate immediately; templates filled later must stay templates
os.path.join → Path /Returns Path, not str; callers doing string ops may break (str(p) at the boundary)
dict[k] init → defaultdictReading a missing key now inserts it; convert with dict(dd) before returning
if os.path.exists → try/exceptMake sure the except catches only the not-found case
requests → httpxDifferent defaults (httpx does not follow redirects by default)
sequential await → gatherOrder of side effects changes; rate limits may be hit

After rewriting: re-run the scanner, run the tests, and spot-check edge inputs (empty list, None, one item, unicode, very large input).

Reporting

text
## Summary
Solid structure, but two production risks: a committed API key and blocking calls in
async handlers. Assumed Python 3.11.

## Findings
| # | Line | Rule  | Severity | Issue                         | Fix                          |
|---|------|-------|----------|-------------------------------|------------------------------|
| 1 | 14   | PY036 | high     | OpenAI key in source          | env var + rotate             |
| 2 | 30   | PY037 | high     | time.sleep in async def       | await asyncio.sleep          |
| 3 | 22   | PY039 | medium   | requests.post without timeout | timeout=(5, 60)              |
| 4 | 46   | PY018 | low      | += string building in loop    | "\n".join(...)               |

## Rewritten code
...

## Behaviour changes
- Requests now raise after 60 s instead of hanging.

## Not changed
- print() in main(): CLI output, intended.

## Next steps
- Add `python scripts/review_idioms.py src --strict` to CI.
- Test for the retry path (mock a 429 then 200).

CI integration

GitHub Actions step:

yaml
- name: Idiom review
  run: python skills/programming/python/python-idiomatic-code-reviewer/scripts/review_idioms.py src --strict --min-severity medium

pre-commit local hook:

yaml
repos:
  - repo: local
    hooks:
      - id: review-idioms
        name: review idioms
        entry: python tools/review_idioms.py --strict
        language: system
        types: [python]

Rollout on a legacy codebase:

  1. Start with --select on the high rules only; fix or # noqa existing hits.
  2. Add medium rules once high is clean.
  3. Keep low rules advisory (no --strict), or run them only on changed files.
More in Python

python-beginner-concept-tutor

Teach Python concepts to beginners heading into AI engineering (data scientists, analysts, students, career switchers): plain-language definitions, analogies, minimal examples, line-by-line walkthroughs, common mistakes, check-your-understanding questions and mini exercises; Socratic hints before solutions; explaining tracebacks; mutable vs immutable, references and aliasing, is vs ==, integer division, floating point, slicing and off-by-one, indentation errors; a staged learning path from variables to calling an LLM API, embeddings and a tiny RAG. Ships a script that lists the concepts a snippet uses in learning order and traces execution step by step like Python Tutor.

python-comprehension-refactorer

Refactor Python loops into list, dict and set comprehensions and generator expressions, and unroll comprehensions that are too clever: filtering, mapping, nested-loop flattening, conditional expressions, walrus in comprehensions, dict inversion, grouping with defaultdict/Counter, lazy generators for sum/any/all/max, streaming large files, itertools (chain, islice, groupby, batched, pairwise, accumulate, product), scoping rules, side-effect anti-patterns and performance (when map/filter or numpy/pandas vectorization win). AI-engineering focus: building prompt lists, batching texts for embeddings, filtering retrieved chunks by score, id->embedding dicts, flattening tool-call results, lazy dataset streaming and token counting.

python-context-manager-writer

Write correct Python context managers and find resource-handling bugs — __enter__/__exit__ semantics and exception suppression, contextlib (contextmanager, asynccontextmanager, ExitStack, AsyncExitStack, closing, aclosing, suppress, redirect_stdout, nullcontext, chdir), async with, re-entrancy, leaked files/connections/clients/pools/locks, and AI-engineering recipes: torch.no_grad/inference_mode/autocast, eval()/train() switches, GPU cache cleanup, token and latency meters, tracing spans, temp env vars, rate-limit semaphores, httpx/aiohttp client lifecycle, FastAPI lifespan, DB/vector-store transactions, atomic checkpoint writes, seeds and agent sandboxes.

python-dataclass-converter

Convert boilerplate Python classes, dicts, tuples and JSON payloads into dataclasses, or into pydantic v2 BaseModel, TypedDict, NamedTuple, attrs or msgspec when that fits better. Covers every @dataclass option (frozen, slots, kw_only, order, unsafe_hash, match_args, weakref_slot), field() (default_factory, init=False, repr, compare, hash, metadata), __post_init__, InitVar, KW_ONLY, inheritance and default ordering, replace(), asdict deep-copy pitfalls, hashing/equality, serialization and validation. AI focus: LLM request/response and chat Message types, RAG chunks with embeddings (numpy eq gotcha), eval records, configs, typed OpenAI/Anthropic JSON.

python-exception-handling-designer

Design and review Python exception handling: custom exception hierarchies with retryable flags, what to catch where, EAFP vs LBYL, chaining with raise ... from, else/finally, logger.exception, add_note, ExceptionGroup and except* with asyncio.TaskGroup, warnings, asserts vs validation, FastAPI error translation, retries with backoff and jitter, timeouts, cancellation, pytest.raises, plus LLM-app failures: 429 rate limits with retry-after, 5xx overloaded, context-length exceeded, content filters, broken streams, malformed JSON repair loops, tool-call errors fed back to the model, partial embedding batch failures, CUDA OOM batch halving, vector DB outages, circuit breakers and model fallbacks.

python-type-hints-annotator

Add accurate, modern type hints to Python and fix mypy / pyright (pyrefly, ty) errors: builtin generics, X | None, TypeAlias and the 3.12 type statement, TypeVar and PEP 695 generics, Protocol, Callable / ParamSpec / Concatenate, TypedDict (Required, NotRequired, ReadOnly), Literal, Final, ClassVar, overload, Annotated, TypeGuard / TypeIs, Self, Never, TypeVarTuple, async types, Iterable vs Sequence vs Mapping, TYPE_CHECKING, stubs, py.typed, gradual strictness. AI-engineering focus: typed LLM message dicts and tool schemas, pydantic vs TypedDict, numpy NDArray, torch / jaxtyping shapes, streaming generators, async clients, callbacks, generic VectorStore protocols, typed config. Ships an AST audit: coverage gate, legacy-typing rewrites, inferred types.