Zen Python
Write Python a senior Pythonista would approve of on first read: concise,
typed, flat, and idiomatic, while matching the target repo's version and style.
Use repo-wide custom instructions for always-on conventions. Use this skill for
deeper Python-specific judgment.
For concrete examples, load only the relevant section from
patterns reference instead of reading the whole
file by default.
Workflows
When writing or refactoring
- Confirm the target Python version and repo conventions first. Infer them
from
pyproject.toml, CI, lockfiles, imports, and surrounding code before
introducing newer syntax.
- Choose the data model before writing logic.
- Write typed signatures early when the repo uses type hints pervasively; in
lighter-weight codebases, keep the typing level consistent with surrounding code.
- Keep the happy path at the shallowest indentation level. Reach for guard
clauses and small helpers before adding more nesting.
- Load only the relevant pattern section from
references/patterns.md when
you need a concrete before/after example.
- Run the self-check before presenting code.
When reviewing
- Start with correctness and regressions: behavior changes, hidden exceptions,
API misuse, resource leaks, concurrency issues, and bad edge-case handling.
- Check compatibility next: supported Python version, stdlib availability,
repo conventions, serialization boundaries, and migration risk.
- Check tests after that: missing coverage for changed behavior, error paths,
async paths, and compatibility-sensitive branches.
- Suggest maintainability or style improvements only after correctness,
compatibility, and testing concerns are covered.
- Load only the relevant pattern section from
references/patterns.md if it
sharpens a concrete review comment.
- Preserve existing behavior when refactoring for style alone.
When to relax these defaults
- Throwaway scripts and REPL exploration: type hints, dataclasses, and
polished error handling are optional. Optimize for speed.
- Performance-critical hot paths: break a style rule if profiling justifies
it. Add a comment explaining the tradeoff.
- Matching an existing codebase: consistency with the project beats local
preferences. Do not force 3.12 idioms into an older or intentionally
conservative repo.
- Prototypes and spikes: keep the code clean, but avoid ceremony that slows
iteration without changing the outcome.
Principles
- Flat over nested. Use guard clauses and early returns so the happy path is easiest to read.
- Explicit over implicit. Prefer clear data flow, names, and interfaces over cleverness.
- Practicality beats purity. Treat these as defaults, not laws. Match the repo and the task.
- Names explain what; comments explain why. Use both intentionally.
- Version-aware advice beats generic advice. Choose syntax and libraries that fit the target Python version.
Typing
- Prefer type hints on function signatures, especially for public APIs, shared helpers, and new modules.
- Prefer
str | None over Optional[str] and built-in generics in 3.10+ codebases. If the repo targets older versions or consistently uses older typing syntax, match the repo.
- Prefer
type UserId = int in 3.12+ codebases. Use TypeAlias or assignment when supporting older versions.
- Prefer
Protocol over ABCs for structural subtyping unless shared behavior, registration, or inheritance-based APIs make an ABC clearer.
- Use
@overload when callers genuinely benefit from narrower signatures. Otherwise keep the callable surface simple.
- Avoid
Any unless the boundary is genuinely dynamic, third-party typed, or intentionally untyped.
Data modeling
Choose the right container before writing logic:
| Situation |
Preferred default |
| Structured data in internal app logic |
@dataclass(slots=True), add frozen=True if immutability helps |
| Fixed set of constants or states |
StrEnum / IntEnum / Enum |
| Data crossing a serialization boundary |
TypedDict or a validation model already used by the repo |
| Need tuple unpacking or tuple-API interop |
NamedTuple |
If you're repeatedly accessing the same internal dict keys, consider a dataclass.
Keep raw dicts or TypedDict when keys are dynamic, external, short-lived, or
primarily about serialization.
Functions
- Prefer one job per function, but do not split a function so aggressively that the flow becomes harder to follow.
- Aim for fewer than 30 lines when it improves readability. Keep a longer function when the whole flow is easier to reason about together.
- Use keyword-only args when call sites become ambiguous, not as a blanket rule.
- Return consistent types. Raise when a
None return would only defer an error downstream.
- Prefer dependency injection over module-level singletons.
Error handling
- Catch specific exceptions when possible.
- Prefer exception chaining when translating or enriching an error. Plain
raise is fine when re-raising unchanged.
- Keep
try blocks tight so unrelated failures do not get folded into the wrong handler.
- Add structured context to custom exceptions when it helps callers, logs, or tests reason about failures.
Async
- Prefer
asyncio.TaskGroup in 3.11+ codebases when you want structured concurrency and fail-fast cancellation.
- Keep
asyncio.gather when the repo targets older versions or when its ordered-results behavior is the clearest fit. Decide intentionally how exceptions should behave.
- Do not call blocking IO (
requests.get, time.sleep, CPU-heavy loops, sync file/network APIs) inside async functions unless the code explicitly offloads it.
- Use an
async_ prefix only when both sync and async variants coexist.
Testing
- Prefer
pytest-style tests with plain functions and @pytest.mark.parametrize when multiple cases share the same shape.
- Name tests as
test_<unit>_<scenario>_<expected> when that fits the repo's existing conventions.
- Test behavior, not implementation details. The tests should survive refactors.
- In review mode, call out missing or weak tests before minor style suggestions.
Docstrings
- Prefer the docstring style already used by the repo. If the repo has no strong convention, default to concise Google-style docstrings.
- Focus on public APIs, non-obvious behavior, and surprising edge cases.
- Keep the first line imperative when that matches the surrounding style.
Gotchas
These are mistakes agents commonly make in Python. Pay special attention:
- Mutable default arguments.
def f(items=[]) shares one list across all calls. Use None plus conditional assignment.
except Exception hiding bugs. Broad handlers can swallow TypeError, KeyError, or programmer mistakes. Catch the narrowest exception you can, or log and re-raise.
os.path vs pathlib. Prefer pathlib.Path in modern codebases unless the repo consistently uses os.path or an API requires plain strings.
- Legacy typing imports. Prefer modern typing syntax in 3.10+ codebases. Keep older imports when version support or repo consistency requires them.
from __future__ import annotations. It is often unnecessary in 3.12+ modules. Do not add or remove it blindly; match the repo's supported versions.
- Forgetting
from in wrapped exceptions. Use raise NewError() from err when adding meaning or context to an exception.
- Returning
None for real errors. Raise when the caller would otherwise hit a less useful failure later.
- Raw dicts as default data containers. Prefer dataclasses for stable internal shapes; keep dicts or
TypedDict for external payloads, dynamic keys, or serialization-heavy layers.
- Silent
pass in except blocks. At minimum log the exception. Prefer handling it intentionally.
gather() without a failure model. Choose TaskGroup for fail-fast structured concurrency or gather when collecting results is intentional.
- Nested comprehensions. Anything deeper than one level is usually harder to read than an explicit loop.
- Stringly-typed comparisons. Repeated literals like
if status == "active" often want an enum or named constant.
Never suggest
type: ignore without an inline explanation.
- Single-letter variables outside comprehensions, lambdas, and tiny local scopes.
- Base classes or mixins before there is a clear shared abstraction.
- God classes. Prefer functions or small focused classes.
from x import *.
- Bare
except: or except Exception: pass.
Self-check before finishing
Before presenting Python code or review findings, verify:
1---2name: zen-python3description: Use this skill to refactor, modernize, review, or make Python code more idiomatic and maintainable. Apply it when the task needs Python-specific judgment about data modeling, typing, exceptions, async structure, tests, or version compatibility, even if the user does not explicitly ask for "Pythonic" code. Do not trigger for trivial syntax questions, isolated one-liners, or routine edits that do not benefit from deeper Python guidance.4---56# Zen Python78Write Python a senior Pythonista would approve of on first read: concise,9typed, flat, and idiomatic, while matching the target repo's version and style.1011Use repo-wide custom instructions for always-on conventions. Use this skill for12deeper Python-specific judgment.1314For concrete examples, load only the relevant section from15[patterns reference](./references/patterns.md) instead of reading the whole16file by default.1718## Workflows1920### When writing or refactoring21221. Confirm the target Python version and repo conventions first. Infer them23 from `pyproject.toml`, CI, lockfiles, imports, and surrounding code before24 introducing newer syntax.252. Choose the data model before writing logic.263. Write typed signatures early when the repo uses type hints pervasively; in27 lighter-weight codebases, keep the typing level consistent with surrounding code.284. Keep the happy path at the shallowest indentation level. Reach for guard29 clauses and small helpers before adding more nesting.305. Load only the relevant pattern section from `references/patterns.md` when31 you need a concrete before/after example.326. Run the [self-check](#self-check-before-finishing) before presenting code.3334### When reviewing35361. Start with correctness and regressions: behavior changes, hidden exceptions,37 API misuse, resource leaks, concurrency issues, and bad edge-case handling.382. Check compatibility next: supported Python version, stdlib availability,39 repo conventions, serialization boundaries, and migration risk.403. Check tests after that: missing coverage for changed behavior, error paths,41 async paths, and compatibility-sensitive branches.424. Suggest maintainability or style improvements only after correctness,43 compatibility, and testing concerns are covered.445. Load only the relevant pattern section from `references/patterns.md` if it45 sharpens a concrete review comment.466. Preserve existing behavior when refactoring for style alone.4748### When to relax these defaults4950- **Throwaway scripts and REPL exploration**: type hints, dataclasses, and51 polished error handling are optional. Optimize for speed.52- **Performance-critical hot paths**: break a style rule if profiling justifies53 it. Add a comment explaining the tradeoff.54- **Matching an existing codebase**: consistency with the project beats local55 preferences. Do not force 3.12 idioms into an older or intentionally56 conservative repo.57- **Prototypes and spikes**: keep the code clean, but avoid ceremony that slows58 iteration without changing the outcome.5960## Principles6162- **Flat over nested.** Use guard clauses and early returns so the happy path is easiest to read.63- **Explicit over implicit.** Prefer clear data flow, names, and interfaces over cleverness.64- **Practicality beats purity.** Treat these as defaults, not laws. Match the repo and the task.65- **Names explain what; comments explain why.** Use both intentionally.66- **Version-aware advice beats generic advice.** Choose syntax and libraries that fit the target Python version.6768## Typing6970- Prefer type hints on function signatures, especially for public APIs, shared helpers, and new modules.71- Prefer `str | None` over `Optional[str]` and built-in generics in 3.10+ codebases. If the repo targets older versions or consistently uses older typing syntax, match the repo.72- Prefer `type UserId = int` in 3.12+ codebases. Use `TypeAlias` or assignment when supporting older versions.73- Prefer `Protocol` over ABCs for structural subtyping unless shared behavior, registration, or inheritance-based APIs make an ABC clearer.74- Use `@overload` when callers genuinely benefit from narrower signatures. Otherwise keep the callable surface simple.75- Avoid `Any` unless the boundary is genuinely dynamic, third-party typed, or intentionally untyped.7677## Data modeling7879Choose the right container before writing logic:8081| Situation | Preferred default |82|---|---|83| Structured data in internal app logic | `@dataclass(slots=True)`, add `frozen=True` if immutability helps |84| Fixed set of constants or states | `StrEnum` / `IntEnum` / `Enum` |85| Data crossing a serialization boundary | `TypedDict` or a validation model already used by the repo |86| Need tuple unpacking or tuple-API interop | `NamedTuple` |8788If you're repeatedly accessing the same internal dict keys, consider a dataclass.89Keep raw dicts or `TypedDict` when keys are dynamic, external, short-lived, or90primarily about serialization.9192## Functions9394- Prefer one job per function, but do not split a function so aggressively that the flow becomes harder to follow.95- Aim for fewer than 30 lines when it improves readability. Keep a longer function when the whole flow is easier to reason about together.96- Use keyword-only args when call sites become ambiguous, not as a blanket rule.97- Return consistent types. Raise when a `None` return would only defer an error downstream.98- Prefer dependency injection over module-level singletons.99100## Error handling101102- Catch specific exceptions when possible.103- Prefer exception chaining when translating or enriching an error. Plain `raise` is fine when re-raising unchanged.104- Keep `try` blocks tight so unrelated failures do not get folded into the wrong handler.105- Add structured context to custom exceptions when it helps callers, logs, or tests reason about failures.106107## Async108109- Prefer `asyncio.TaskGroup` in 3.11+ codebases when you want structured concurrency and fail-fast cancellation.110- Keep `asyncio.gather` when the repo targets older versions or when its ordered-results behavior is the clearest fit. Decide intentionally how exceptions should behave.111- Do not call blocking IO (`requests.get`, `time.sleep`, CPU-heavy loops, sync file/network APIs) inside `async` functions unless the code explicitly offloads it.112- Use an `async_` prefix only when both sync and async variants coexist.113114## Testing115116- Prefer `pytest`-style tests with plain functions and `@pytest.mark.parametrize` when multiple cases share the same shape.117- Name tests as `test_<unit>_<scenario>_<expected>` when that fits the repo's existing conventions.118- Test behavior, not implementation details. The tests should survive refactors.119- In review mode, call out missing or weak tests before minor style suggestions.120121## Docstrings122123- Prefer the docstring style already used by the repo. If the repo has no strong convention, default to concise Google-style docstrings.124- Focus on public APIs, non-obvious behavior, and surprising edge cases.125- Keep the first line imperative when that matches the surrounding style.126127## Gotchas128129These are mistakes agents commonly make in Python. Pay special attention:130131- **Mutable default arguments.** `def f(items=[])` shares one list across all calls. Use `None` plus conditional assignment.132- **`except Exception` hiding bugs.** Broad handlers can swallow `TypeError`, `KeyError`, or programmer mistakes. Catch the narrowest exception you can, or log and re-raise.133- **`os.path` vs `pathlib`.** Prefer `pathlib.Path` in modern codebases unless the repo consistently uses `os.path` or an API requires plain strings.134- **Legacy typing imports.** Prefer modern typing syntax in 3.10+ codebases. Keep older imports when version support or repo consistency requires them.135- **`from __future__ import annotations`.** It is often unnecessary in 3.12+ modules. Do not add or remove it blindly; match the repo's supported versions.136- **Forgetting `from` in wrapped exceptions.** Use `raise NewError() from err` when adding meaning or context to an exception.137- **Returning `None` for real errors.** Raise when the caller would otherwise hit a less useful failure later.138- **Raw dicts as default data containers.** Prefer dataclasses for stable internal shapes; keep dicts or `TypedDict` for external payloads, dynamic keys, or serialization-heavy layers.139- **Silent `pass` in except blocks.** At minimum log the exception. Prefer handling it intentionally.140- **`gather()` without a failure model.** Choose `TaskGroup` for fail-fast structured concurrency or `gather` when collecting results is intentional.141- **Nested comprehensions.** Anything deeper than one level is usually harder to read than an explicit loop.142- **Stringly-typed comparisons.** Repeated literals like `if status == "active"` often want an enum or named constant.143144## Never suggest145146- `type: ignore` without an inline explanation.147- Single-letter variables outside comprehensions, lambdas, and tiny local scopes.148- Base classes or mixins before there is a clear shared abstraction.149- God classes. Prefer functions or small focused classes.150- `from x import *`.151- Bare `except:` or `except Exception: pass`.152153## Self-check before finishing154155Before presenting Python code or review findings, verify:156157- [ ] Target Python version and repo conventions were checked before introducing newer syntax.158- [ ] Signatures follow the repo's typing level, and new code is typed where it matters.159- [ ] No mutable default arguments.160- [ ] No bare `except` or silent `except ... pass`.161- [ ] Wrapped exceptions use `from` when adding context.162- [ ] `try` blocks wrap only the lines that can raise.163- [ ] Data containers match the use case instead of defaulting to raw dicts.164- [ ] Nesting is shallow where practical; guard clauses are used when they clarify the flow.165- [ ] Path handling matches the repo style; prefer `pathlib` in modern code.166- [ ] Async code uses `TaskGroup` or `gather` intentionally.167- [ ] In review mode, correctness, compatibility, and test coverage come before style suggestions.