# Zen Python

> 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.

- Skill: `pavan139/zen-python` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add pavan139/zen-python`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pavan139/zen-python/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: pavan139 (https://skillmd.com/u/pavan139)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/pavan139/zen-python

---


# 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](./references/patterns.md) instead of reading the whole
file by default.

## Workflows

### When writing or refactoring

1. Confirm the target Python version and repo conventions first. Infer them
   from `pyproject.toml`, CI, lockfiles, imports, and surrounding code before
   introducing newer syntax.
2. Choose the data model before writing logic.
3. Write typed signatures early when the repo uses type hints pervasively; in
   lighter-weight codebases, keep the typing level consistent with surrounding code.
4. Keep the happy path at the shallowest indentation level. Reach for guard
   clauses and small helpers before adding more nesting.
5. Load only the relevant pattern section from `references/patterns.md` when
   you need a concrete before/after example.
6. Run the [self-check](#self-check-before-finishing) before presenting code.

### When reviewing

1. Start with correctness and regressions: behavior changes, hidden exceptions,
   API misuse, resource leaks, concurrency issues, and bad edge-case handling.
2. Check compatibility next: supported Python version, stdlib availability,
   repo conventions, serialization boundaries, and migration risk.
3. Check tests after that: missing coverage for changed behavior, error paths,
   async paths, and compatibility-sensitive branches.
4. Suggest maintainability or style improvements only after correctness,
   compatibility, and testing concerns are covered.
5. Load only the relevant pattern section from `references/patterns.md` if it
   sharpens a concrete review comment.
6. 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:

- [ ] Target Python version and repo conventions were checked before introducing newer syntax.
- [ ] Signatures follow the repo's typing level, and new code is typed where it matters.
- [ ] No mutable default arguments.
- [ ] No bare `except` or silent `except ... pass`.
- [ ] Wrapped exceptions use `from` when adding context.
- [ ] `try` blocks wrap only the lines that can raise.
- [ ] Data containers match the use case instead of defaulting to raw dicts.
- [ ] Nesting is shallow where practical; guard clauses are used when they clarify the flow.
- [ ] Path handling matches the repo style; prefer `pathlib` in modern code.
- [ ] Async code uses `TaskGroup` or `gather` intentionally.
- [ ] In review mode, correctness, compatibility, and test coverage come before style suggestions.

