A good Python setup for Claude Code has three parts: a short CLAUDE.md that tells it which commands to run, a hook that runs your linters after every edit so it can't forget, and a fast test command it can loop on. This guide uses uv, ruff, mypy and pytest, but the pattern works with any toolchain.
Put it at ./CLAUDE.md or ./.claude/CLAUDE.md. Run /init to generate a starting point, then trim it. Claude Code's docs suggest keeping each file under about 200 lines and making instructions concrete enough to verify. See the CLAUDE.md template for a fuller structure.
# Project
## Commands
- Install: `uv sync`
- Run anything through uv: `uv run <cmd>` (never bare `python` or `pip`)
- Add a dependency: `uv add <pkg>` (dev: `uv add --dev <pkg>`); never edit uv.lock by hand
- Lint: `uv run ruff check .` Format: `uv run ruff format .`
- Types: `uv run mypy src`
- Tests: `uv run pytest -q`; one test: `uv run pytest -q path/to/test_x.py -k name`
## Conventions
- Python 3.12, type hints on all public functions
- Source in `src/`, tests mirror it under `tests/`
- Never modify or skip existing tests to make them pass; ask first
CLAUDE.md is context, not enforcement. Claude usually follows it, but anything that must happen every time belongs in a hook.
Keep tool config in the repo so Claude, you and CI all agree:
[tool.ruff]
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
[tool.mypy]
strict = true
files = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q"
A PostToolUse hook runs after Claude writes a file. If it exits with code 2, its stderr is shown to Claude, so type errors come back as feedback it can fix immediately. Add this to .claude/settings.json (committed, so the team shares it):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/py-check.sh", "timeout": 120 }
]
}
]
}
}
The script reads the JSON payload from stdin and takes tool_input.file_path:
#!/usr/bin/env bash
# .claude/hooks/py-check.sh (chmod +x it)
f=$(python3 -c 'import json,sys;print(json.load(sys.stdin).get("tool_input",{}).get("file_path",""))')
case "$f" in *.py) ;; *) exit 0 ;; esac
cd "$CLAUDE_PROJECT_DIR" || exit 0
uv run ruff format -q "$f"
uv run ruff check --fix -q "$f" || { echo "ruff found unfixable issues in $f" >&2; exit 2; }
case "$f" in
*/src/*) uv run mypy "$f" 2>&1 >&2 || exit 2 ;;
esac
exit 0
Test it without Claude first:
echo '{"tool_input":{"file_path":"'"$PWD"'/src/app.py"}}' | .claude/hooks/py-check.sh; echo "exit=$?"
Run /hooks in Claude Code to confirm it is registered. You can also generate and test hook configs with the free hook builder.
Don't run the whole suite in a hook; it is slow and runs on every edit. Instead, tell Claude to run the narrowest test that covers the change, then the full suite before it finishes:
Fix the bug described below. First write a failing pytest that reproduces it and
show me it fails. Then fix it, run that test, then run `uv run pytest -q`.
Do not edit existing tests to make them pass.
For the full red-green workflow, see test-driven development with Claude Code.
| Mistake | Fix |
|---|---|
Claude runs bare pip install or python | State "always use uv run / uv add" in CLAUDE.md |
Hook fails with command not found | Use uv run ruff, not ruff, so the project environment is used |
| mypy on one file reports errors from other modules | Expected. Fix the cause, or use mypy src if you prefer whole-project checks |
| Hook blocks on a legacy file full of errors | Scope the case pattern to new code, or add per-file ignores in pyproject.toml |
Hook path breaks after Claude cds | Reference scripts via "$CLAUDE_PROJECT_DIR" |
.claude/rules/ files that use paths frontmatter.Keelwork bundles 10 workflow skills, 5 tested safety hooks (including a full guard-bash and a secret scanner), 3 subagents and 5 CLAUDE.md templates, with a one-command installer that safely merges into your settings.
Get Keelwork — $24 →