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

python-idiomatic-code-reviewer

Review Python code for non-idiomatic, unpythonic or risky patterns and rewrite it into clean modern Python: PEP 8 / PEP 20, EAFP vs LBYL, iteration idioms (enumerate, zip, comprehensions, any/all), unpacking, f-strings, pathlib, truthiness, `is None`, mutable defaults, walrus, match statements, collections / itertools / functools, logging instead of print, with-blocks and resource handling. Includes AI-engineering checks: hardcoded API keys, time.sleep or requests inside async def, missing HTTP timeouts, unbounded retry loops, model clients built at import time, prompt string concatenation, sequential awaits, eval() on LLM output. Ships a 46-rule AST reviewer with severities and suggested rewrites. Use when the user asks to review, clean up, modernise, make pythonic or idiomatic, refactor or code-review a .py file, LLM / RAG / agent / FastAPI / training code, a pull request diff, or asks 'is this good Python?' or 'how would a senior Python dev write this?'.

Programming / Pythonv1.0.0Updated OCT 20 20265 references1 script

Review Python the way a senior Python and AI engineer would: find the patterns that are wrong (mutable defaults, swallowed exceptions, blocking calls in async code, leaked keys), the patterns that are fragile (== None, type(x) == T, check-then-open, unbounded retries) and the patterns that are merely un-Pythonic (range(len(x)), manual counters, %-formatting, os.path). Then give a graded, line-referenced review and a rewritten version that keeps behaviour identical. Static checks come from scripts/review_idioms.py; judgement, context and the rewrite come from you.

Trigger Conditions

Activate this skill when the user:

  • Pastes Python and asks "review this", "make it pythonic", "clean this up", "modernise", "is this idiomatic?"
  • Asks for a code review of a .py file, a PR diff or a notebook cell
  • Works on LLM client wrappers, RAG pipelines, agents, tool calling, eval harnesses, FastAPI model services, training or batch inference loops and wants them production-ready
  • Asks about a specific idiom: EAFP vs LBYL, walrus, match, enumerate/zip, defaultdict, pathlib, f-strings, is None
  • Reports async code that is "slow" or "hangs" (often time.sleep / requests inside async def, or no timeouts)
  • Wants to upgrade a codebase to a newer Python (3.9 baseline, 3.10 match, 3.11 TaskGroup, 3.12 type syntax)

Workflow

text
1. SCOPE     -> Python version target? library, service, script, or notebook? hot path or one-off?
2. SCAN      -> python scripts/review_idioms.py <file|dir|-> (add --json for tooling)
3. TRIAGE    -> confirm each finding in context; drop false positives; add what static analysis misses
4. GRADE     -> high (bug/security/outage), medium (fragile/likely bug), low (style/idiom)
5. REWRITE   -> smallest behaviour-preserving change per finding; keep public signatures
6. VERIFY    -> re-run the script; run the tests; diff old vs new behaviour on edge cases
7. REPORT    -> findings table, rewritten code, notes on anything deliberately left alone

Severity Model

SeverityMeaningExamplesMerge?
highWrong results, security hole, outage or data lossmutable default, bare/swallowing except, hardcoded key, time.sleep in async def, eval on model output, unbounded retryBlock
mediumFragile; correct today, breaks on edge cases or under load== None, type(x) == T, open() without with, range(len()), missing HTTP timeout, client built at importFix before merge
lowIdiom, readability, consistency%-format, os.path, manual counter, len(x) == 0, typing.ListSuggest; batch

--strict fails (exit 1) on any medium or high finding, which makes it a reasonable CI gate. Full grading guidance and comment templates are in review-checklist.md.

Core Rules

1. Correctness first

python
# Bad: one list shared by every call; chat histories bleed between users
def chat(prompt, history=[]):
    history.append({"role": "user", "content": prompt})
    return history

# Good
def chat(prompt: str, history: list[dict] | None = None) -> list[dict]:  # `X | None` in annotations: 3.10+, or 3.9 with `from __future__ import annotations`
    history = [] if history is None else history
    history.append({"role": "user", "content": prompt})
    return history
SmellWhy it is wrongFix
except: / except Exception: passHides rate limits, auth errors, bugs; bare except also traps Ctrl-CCatch the specific errors; log with logger.exception; re-raise
raise NewError(...) inside exceptOriginal traceback is chained implicitly as "during handling"; intent unclearraise NewError(...) from err (or from None)
x == None__eq__ can be overridden; numpy returns an arrayx is None
type(x) == dictRejects subclasses (OrderedDict, pydantic types)isinstance(x, dict) or collections.abc.Mapping
f = open(p)Leaks the handle on exceptionswith open(p, encoding="utf-8") as f: or Path(p).read_text(encoding="utf-8")
eval(llm_output)Remote code execution via prompt injectionjson.loads, ast.literal_eval, pydantic model_validate_json

2. EAFP over LBYL (when the check can race or duplicates work)

python
# LBYL: file can disappear between exists() and open(); two syscalls
if os.path.exists(cache):
    with open(cache) as f:
        return json.load(f)
return {}

# EAFP
try:
    return json.loads(Path(cache).read_text(encoding="utf-8"))
except FileNotFoundError:
    return {}

Use LBYL when the check is cheap, has no race and the failure is the common case (e.g. if key in mapping in a tight loop where misses dominate). Use EAFP for I/O, external services, dict access where hits dominate, and duck typing.

3. Iteration idioms

Instead ofWrite
for i in range(len(xs)): x = xs[i]for x in xs:
i = 0 ... i += 1for i, x in enumerate(xs, start=1):
for i in range(len(a)): f(a[i], b[i])for x, y in zip(a, b, strict=True): (strict: 3.10+)
for k in d.keys(): v = d[k]for k, v in d.items():
out = [] + for + appendout = [f(x) for x in xs if cond(x)]
for x in xs: if p(x): return True / return Falsereturn any(p(x) for x in xs)
if k not in d: d[k] = [] then d[k].append(v)d = defaultdict(list); d[k].append(v)
Nested loops over batchesitertools.batched(xs, n) (3.12+) or a slicing helper
s += piece in a loop"".join(pieces)
sorted(xs)[0] / sorted(xs)[-1]min(xs) / max(xs); heapq.nlargest(k, xs, key=...) for top-k
python
# Top-k retrieval scores without sorting everything
import heapq
top = heapq.nlargest(5, zip(scores, doc_ids))          # [(score, id), ...]

4. Unpacking, truthiness, comparisons

python
first, *rest = tokens                   # star unpacking
role, content = message["role"], message["content"]
(prompt_tokens, completion_tokens) = usage

if docs: ...                            # not: if len(docs) > 0
if not results: ...                     # not: if len(results) == 0
if value is None: ...                   # not: if value == None
if 0 < temperature <= 2: ...            # chained comparison
if model in {"gpt-4o", "claude-sonnet-4-5"}: ...   # set membership for literal choices

Truthiness caveat for AI code: if embedding: raises ValueError on a numpy array with more than one element, and if df: raises on a DataFrame. Use arr.size, len(arr), df.empty or tensor.numel(). Same for == True on masks: use the mask itself.

5. Strings

  • f-strings everywhere for interpolation; f"{x!r}", f"{n:,}", f"{dt:%Y-%m-%d}", f"{x = }" (3.8+) for debug.
  • Logging is the exception: logger.info("loaded %d docs", n) defers formatting.
  • Build large text (context windows, CSV, prompts) with "\n".join(parts).
  • Prompt templates live in one constant with named fields (str.format_map, string.Template, Jinja) rather than in + chains spread over a function.
  • str.removeprefix / removesuffix (3.9+) instead of slicing with len(prefix).

6. Resources and context managers

python
with open(src, encoding="utf-8") as fin, open(dst, "w", encoding="utf-8") as fout:
    for line in fin:
        fout.write(line)

# 3.10+: parenthesised context managers across lines
with (
    httpx.Client(timeout=30) as client,
    open("out.jsonl", "w", encoding="utf-8") as out,
):
    ...

# Variable number of resources
from contextlib import ExitStack
with ExitStack() as stack:
    files = [stack.enter_context(open(p, encoding="utf-8")) for p in paths]

Always pass encoding="utf-8" for text files (PEP 597; the default is the locale encoding, cp1252 on many Windows machines).

7. Structure

  • pathlib.Path over os.path / glob: Path(root) / "data" / "train.jsonl", p.suffix, p.read_text(), p.glob("**/*.md").
  • @dataclass(slots=True, frozen=True) (slots: 3.10+) for config/value objects; pydantic for validated I/O boundaries.
  • enum.Enum / StrEnum (3.11+) for fixed choices instead of magic strings.
  • match (3.10+) for structural dispatch (message roles, tool-call shapes, AST nodes); dicts for plain value-to-function maps.
  • No from x import *, no global, no lambdas bound to names, no shadowing builtins (id, type, input, list).
  • Builtin generics list[str], dict[str, int] on 3.9+; X | None on 3.10+ (or with from __future__ import annotations in annotations only).

8. Logging, not print

python
logger = logging.getLogger(__name__)           # module level, cheap, no handlers
logger.debug("retrieved %d chunks for %r", len(chunks), query)
logger.exception("tool call failed")           # inside except: includes the traceback

Configure handlers once in the entry point (logging.basicConfig(level=...)), never in library modules. print is fine in CLIs' main() and in notebooks.

AI-Engineering Rules

IdPatternWhy it mattersFix
PY036api_key = "sk-..."Keys leak via git, logs, notebooks; bots scrape GitHub within minutesos.environ["OPENAI_API_KEY"], pydantic-settings SecretStr; rotate the key
PY037time.sleep() in async defFreezes every coroutine on the loop (all users of the service)await asyncio.sleep()
PY038requests / subprocess.run in async defSame: blocking I/O on the event loophttpx.AsyncClient, asyncio.create_subprocess_exec, asyncio.to_thread
PY039requests.post(url) with no timeoutDefault is no timeout; one stalled socket hangs a worker forevertimeout=(5, 60)
PY040while True: + try/except + sleepA permanent 401/400 retries forever and burns quotaBounded attempts, exponential backoff + jitter, retry only retryable errors
PY041client = OpenAI() / SentenceTransformer(...) at module levelImport-time network/GPU work; breaks tests, forked workers, config overridesfunctools.cache factory or FastAPI lifespan + dependency
PY042OpenAI() without timeout=SDK defaults to a 10-minute timeout with silent retriesOpenAI(timeout=30, max_retries=3)
PY043prompt = "..." + ctx + "..."Hard to read, diff, test and versionTemplate constant with named fields
PY044for x in xs: await call(x)N sequential round-tripsasyncio.gather / TaskGroup behind a Semaphore
PY004eval(model_output)Prompt injection becomes code executionjson.loads + schema validation
PY034print() debugging in pipelinesNo levels, no request ids, lost in workerslogging (structured where possible)
python
# Bounded retry with backoff and jitter, retrying only what is retryable
import random, time
from openai import APIConnectionError, RateLimitError

RETRYABLE = (RateLimitError, APIConnectionError)

def complete(client, **kwargs):
    for attempt in range(5):
        try:
            return client.chat.completions.create(**kwargs)
        except RETRYABLE:
            if attempt == 4:
                raise
            time.sleep(min(30, 2 ** attempt) + random.random())

Deeper patterns (lifespan-managed clients, streaming, fan-out with semaphores, structured output parsing, batching, settings) are in ai-engineering-idioms.md.

What Static Analysis Misses (review by hand)

  • Behaviour: is the rewrite actually equivalent? (any() short-circuits like the loop; a comprehension does not keep the loop variable after it; dict.get(k, default) differs from try/except KeyError when the stored value is None.)
  • Naming and decomposition: 80-line functions, boolean flag parameters, unclear names.
  • Concurrency: shared mutable state across threads/tasks, missing await, unawaited coroutines, sync SDK clients used from async code through method calls (client.chat.completions.create inside async def).
  • Data: float equality on scores, off-by-one in chunk overlap, tokenizer vs character lengths.
  • API contracts: returning None vs raising, mixed return types, mutable return values that callers mutate.
  • Tests: is the risky path covered?

Rewrite Discipline

  1. Preserve behaviour first, style second. If a fix changes semantics (e.g. tightening except Exception to specific errors), say so explicitly.
  2. Keep public names and signatures unless the user asked for an API change; deprecate rather than rename.
  3. Match the project's minimum Python version. Do not introduce match, X | Y at runtime, TaskGroup or itertools.batched below their versions (modern-python-by-version.md).
  4. One concern per change; avoid drive-by reformatting that buries the real fix in a diff.
  5. Prefer stdlib power tools before new dependencies (stdlib-power-tools.md).
  6. Do not "fix" deliberate patterns: print in a CLI's main, LBYL in hot loops where misses dominate, type(x) is bool when bools must be excluded from ints, module-level clients when the team intends a singleton and has accepted the trade-off.

Output Format

Return the review in this shape:

  1. Summary: one or two sentences: overall quality, the most important risk, Python version assumed.

  2. Findings table, most severe first:

    #LineRuleSeverityIssueFix
    114PY036highAPI key hardcodedRead OPENAI_API_KEY from env; rotate key
  3. Rewritten code: the full revised file or function, behaviour-preserving, with short comments only where a change is non-obvious.

  4. Behaviour changes: anything the rewrite changes on purpose (exceptions now propagate, timeouts now raise, etc.).

  5. Not changed: findings deliberately left alone, with the reason.

  6. Next steps: tests to add, CI gate (--strict), follow-up refactors.

For a quick question about one idiom, skip the table: give the before/after and one line of why.

Scripts

Static review with 46 rules (PY001–PY046), AST + tokenize based, never executes the code:

bash
python scripts/review_idioms.py app.py                       # human-readable, with suggested rewrites
python scripts/review_idioms.py src/                         # every *.py under a folder
cat app.py | python scripts/review_idioms.py -               # stdin
python scripts/review_idioms.py app.py --json                # {"files", "summary", "findings", "errors"}
python scripts/review_idioms.py app.py --strict              # exit 1 on any medium/high finding (CI gate)
python scripts/review_idioms.py app.py --min-severity medium # hide low findings
python scripts/review_idioms.py app.py --select PY036,PY037,PY038,PY039,PY040   # AI/production risks only
python scripts/review_idioms.py app.py --ignore PY03         # skip PY030-PY039 (prefixes and names work)
python scripts/review_idioms.py app.py --no-fix              # findings only, no rewrite snippets
python scripts/review_idioms.py --list-rules                 # id, severity, name, title for every rule

Each finding carries path:line:col, rule id, severity, a specific message and a before/after rewrite snippet. Suppress a single line with # noqa: PY034 (or bare # noqa). Exit codes: 0 = done, 1 = --strict and a medium/high finding, 2 = usage error, unreadable file or syntax error.

Rule groups: PY001–PY011 correctness and risk; PY012–PY018 iteration idioms; PY019–PY035 and PY045–PY046 modern style; PY036–PY044 AI engineering.

Additional Resources

FileContent
idioms-catalog.md60+ before/after idioms: iteration, unpacking, strings, dicts, comparisons, exceptions, functions, classes, files
ai-engineering-idioms.mdLLM clients, secrets, async fan-out, retries, timeouts, streaming, prompts, structured output, batching, logging, training loops
review-checklist.mdHow to run a review, severity grading, false positives per rule, writing review comments, CI setup
modern-python-by-version.md3.8 → 3.13 features that change idiomatic style, with version gates
stdlib-power-tools.mdcollections, itertools, functools, pathlib, contextlib, dataclasses, enum, operator, heapq, bisect quick wins
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.