10 CLAUDE.md Rules Every Python Developer Needs in 2026
You ask Claude to "add a function that fetches a user from the API and saves to the DB" inside a Python project, and you get back:
- A
requests.get(...)call with no timeout, no retry, noraise_for_status() - A bare
except:that swallows the exception and returnsNone - A function with no type hints —
def fetch_user(id):returningdict -
print(f"saving user {user}")instead of structured logging - A test that reaches into
os.environand mutates global state -
pip install requestssuggested in the README, butpyproject.tomlis never touched
The model isn't broken. It's defaulting to the median of fifteen years of Python tutorials. Your repo doesn't look like the median of fifteen years of Python tutorials — but Claude doesn't know that until you tell it.
A CLAUDE.md file at the root of your repo is the cheapest leverage you have. Claude Code reads it on every task. Cursor, Aider, and any tool that respects context files reads it too. Write the rules once, stop fighting the same fights every PR.
Here are 10 rules I drop into every Python repo in 2026.
Rule 1 — Type Hints Are Mandatory, Not Decorative
Why: Without hints, Claude treats every parameter as Any. You lose IDE help, mypy can't catch regressions, and refactors become guesswork.
## Type hints
- Every function signature is fully annotated, including return type.
- Use `from __future__ import annotations` at the top of every module.
- Prefer `list[int]`, `dict[str, User]`, `X | None` over `List`, `Dict`, `Optional`.
- No `Any` without a `# type: ignore[reason]` comment explaining why.
- Public APIs are checked with `mypy --strict`.
Rule 2 — One Lockfile, One Tool, No pip install In Prose
Why: Claude will tell users to pip install foo in READMEs and forget to update pyproject.toml. Six months later nobody can reproduce the environment.
## Dependencies
- This project uses `uv` (or `poetry`, or `pdm` — pick one and say so).
- Add deps with `uv add foo`. Never edit `pyproject.toml` by hand for versions.
- The lockfile (`uv.lock`) is committed.
- Never suggest `pip install` in docs, comments, or commit messages.
- Dev-only deps go under the `dev` group, not `dependencies`.
If you're starting a new project in 2026, default to uv. It's fast, deterministic, and replaces pip, pip-tools, virtualenv, and pyenv in one binary.
Rule 3 — Errors Are Specific, Never Bare
Why: except Exception: and except: are how production bugs become silent data loss. Claude reaches for them because they "make the test green."
## Error handling
- Never use bare `except:` or `except Exception:` without re-raising.
- Catch the narrowest exception that fits — `KeyError`, `httpx.TimeoutException`, etc.
- Re-raise with `raise CustomError(...) from e` to preserve the chain.
- Domain errors live in `app/errors.py` and inherit from a single `AppError` base.
- Never `return None` on error. Raise, or return a typed `Result`.
Rule 4 — Logging, Not print
Why: print calls leak secrets, can't be silenced in tests, and don't carry structured context. Claude defaults to print because tutorials do.
## Logging
- Use `logging.getLogger(__name__)` at module top. Never `print()` outside scripts in `bin/`.
- Use structured logging: `log.info("user_created", extra={"user_id": user.id})`.
- Never log secrets, tokens, or full request bodies.
- Configuration lives in one place (`app/logging_config.py`). Modules don't call `logging.basicConfig`.
Rule 5 — Tests Use pytest, Fixtures Over Setup, No Network
Why: Without rules, Claude generates unittest.TestCase classes, mocks the world, and writes tests that hit real APIs. You end up with a flaky CI suite that passes locally and fails on Tuesdays.
## Testing
- All tests use `pytest`. No `unittest.TestCase` classes.
- Shared state lives in fixtures, not `setUp` methods.
- One assertion concept per test. Use `pytest.mark.parametrize` for variants.
- No real network calls. Use `respx` (httpx) or `responses` (requests) to stub.
- Database tests use a real Postgres via `pytest-postgresql` or testcontainers, not mocks.
- Coverage floor: 85% on `app/`, enforced in CI.
Rule 6 — ruff And black Are Non-Negotiable
Why: Style debates eat code review time. AI assistants generate code that "looks reasonable" but doesn't match the repo's actual conventions. Tooling enforces what humans can't.
## Formatting & linting
- Format with `ruff format` (or `black`). Lint with `ruff check`.
- Config lives in `pyproject.toml` under `[tool.ruff]`.
- Pre-commit hook runs both. CI fails on any lint warning.
- Don't disable rules per-line without a `# noqa: E501` plus reason.
- Imports are sorted by `ruff` (replaces `isort`).
Rule 7 — pyproject.toml Is The Source Of Truth
Why: Old projects scatter config across setup.py, setup.cfg, requirements.txt, tox.ini, .flake8, pytest.ini. Claude will happily add a new config file rather than consolidate. Stop the bleeding.
## Project metadata
- All tool config lives in `pyproject.toml`. No `setup.py`, no `setup.cfg`, no `requirements.txt`.
- Build backend is `hatchling` (or `setuptools` if legacy). Stated explicitly.
- Python version pin lives in `requires-python` and matches CI matrix.
- Entry points use `[project.scripts]`, never custom `bin/` scripts that import from src.
Rule 8 — Async Is A Choice, Not An Accident
Why: Mixing async def with blocking I/O is the silent killer of Python web apps. Claude will write async def then call requests.get() because both look like "the Python way."
## Async rules
- A function is fully `async def` (every awaited call is non-blocking) or fully sync.
- Inside `async def`: no `requests`, no sync DB drivers, no `time.sleep`, no sync file I/O.
- Use `httpx.AsyncClient`, `asyncio.sleep`, async SQLAlchemy, `aiofiles`.
- If a sync-only library is unavoidable, wrap with `await asyncio.to_thread(fn, ...)`.
- The event loop is created by the framework — never `asyncio.run()` inside library code.
Rule 9 — Configuration Is Typed And Loaded Once
Why: os.environ.get("API_KEY", "") scattered across thirty files is how a missing env var becomes a 500 in prod. AI assistants love this pattern because it's what they saw most.
## Configuration
- All env vars are loaded via a single `Settings` class (Pydantic `BaseSettings` or `dataclasses`).
- The settings instance is imported, never re-read from `os.environ`.
- Secrets have NO default. A missing secret must crash on startup, not at request time.
- `.env.example` is committed and lists every variable. `.env` is gitignored.
- Different environments don't branch in code — they load different settings classes.
Rule 10 — Boundaries: What Claude Must Not Touch
Why: Without explicit boundaries, AI assistants "helpfully" rewrite migrations, regenerate lockfiles, or refactor legacy code that's load-bearing. Tell it where to stop.
## Boundaries
- Do NOT modify files in `migrations/` once they've been applied to any environment.
- Do NOT edit `uv.lock` directly — use `uv add` / `uv lock`.
- Do NOT refactor `app/legacy/` — migration in progress, scope it out.
- Do NOT add new top-level dependencies without explicit approval.
- One logical change per commit. If a task spans modules, split it.
A Minimal CLAUDE.md Skeleton
If you want to start today, drop this at the root:
# CLAUDE.md
## Stack
- Python 3.12, uv, ruff, pytest, mypy --strict
- Pydantic v2, httpx, structlog
## Rules
- Type hints mandatory, `mypy --strict` passes
- `uv` is the only dependency tool
- No bare `except:`. No `print()` outside `bin/`
- All tests are `pytest`. No real network calls.
- `ruff format` + `ruff check` pass before commit
- Settings loaded once via Pydantic `BaseSettings`
## Boundaries
- No edits to `migrations/`, `uv.lock`, or `app/legacy/`
- One logical change per commit
- Ask before adding dependencies
That alone will fix 80% of the bad code Claude generates in a fresh Python repo.
Where To Go From Here
These 10 rules are the foundation. The full CLAUDE.md files I run on production Python projects cover another forty rules — packaging, security headers, dependency injection patterns, structured logging schemas, CI matrix conventions, and framework-specific rules for Django, FastAPI, Flask, and Celery.
If you want the whole pack — battle-tested rules across Python, TypeScript, React, Next.js, Go, Rust, Docker, and more — I've put it together as the CLAUDE.md Rules Pack. One file per stack, drop it in your repo, stop arguing with your AI.
Or just steal the 10 above. They're the highest-leverage ones.
The developers getting the best output from Claude in 2026 aren't the ones writing longer prompts. They're the ones writing better CLAUDE.md files.
Top comments (0)