python-project-best-practice¶
An opinionated baseline for Python projects that both people and agents work in — and the two entry points that get you there: scaffold a new project, or score and modernize an existing one. Same conventions, one skill.
The bias throughout: every operation has one canonical, discoverable, non-interactive command. That is what makes a repo workable by an agent, and it happens to be what makes it pleasant for humans.
Scope note: this skill deliberately does not restate
verifiable-surfaces(CLI surface design),marimo-batch-mlflow(dual-mode notebooks),fastapi-ai-patterns(production FastAPI), ormkdocs-site-bootstrap(docs sites). It links to them and fills the gap none of them covered: the project itself.
The defaults it argues for¶
| Concern | Default | Why not the alternative |
|---|---|---|
| Environment | uv + committed uv.lock + .python-version |
poetry/pip-tools/conda all solve less of it; uv.lock never constrains your users |
| Layout | src/<pkg>/ |
a flat layout imports the source dir, so missing __init__.py / package data passes tests and breaks installs |
| Dev deps | PEP 735 [dependency-groups] |
an extra is published; your test runner should not be in your users' dependency graph |
| Lint + format | ruff check + ruff format |
ruff format is a black reimplementation — running both makes them fight |
| Types | ty (pinned exactly), mypy documented as the conservative swap |
ty is 0.0.x: fast enough that people run it, unstable enough that >= is a trap |
| CLI | Tyro, one frozen dataclass per subcommand | commands as data are testable without argv or subprocesses |
| Completion | --tyro-write-completion |
built in; shtab/argcomplete wiring is redundant |
| Logging | loguru at entry points, stdlib in library code | a library that calls loguru.logger.add() hijacks the importing program's logging |
| Config | pydantic-settings, defaults < .env < environment |
and <tool> info prints what actually resolved |
| Task runner | Justfile (taskipy noted) |
just needs no Python, so just setup can bootstrap the venv itself |
What ships¶
- The full SKILL.md (SKILL.md) — decision spine, two workflows, and a 22-item gotchas section.
- Nine references, loaded on demand:
uv-and-pyproject.md— commands, groups vs extras, lockfile policy, interpreter pinning, build backends,uv tool install/ Trusted Publishing, workspaces.tyro-cli.md— dataclass commands, subcommand unions, module splits, completion, config objects.quality-gates.md— ruff rule selection,tyvs mypy, pytest layout, coverage placement, CI, pre-commit.logging-and-config.md— the app/library layering rule, the three loguru traps, pydantic-settings, direnv.notebooks-and-widgets.md— where notebooks live, dual mode, ruff conflicts, packaged anywidgets.api-and-services.md— the FastAPI stub's boundary, and a decision rule for skill vs MCP (usually: neither, add a subcommand).rust-pyo3.md— maturin layout, abi3, the stale-binary trap, why type checkers need a hand-written.pyi.agent-interface.md— AGENTS.md vs README vs the package's own skill, the symlink rule, the docs-drift gate.legacy-refactor.md— the migration ladder, one rung per PR, and what not to migrate.
- Two PEP 723 scripts (
uv run, inline deps, JSON on stdout):new-python-project.py— scaffold from the bundled template tree.--profile {minimal,cli,lib,api,ml,rust},--dry-run,--force,--no-git. Offline; writes nothing outside the target directory.audit-python-project.py— 26 read-only checks with evidence and a fix hint, plus an orderedmigration_plan[].--format {json,table},--fail-on {fail,warn,never}. Deliberately has no--fix.
- A template tree (
assets/project/, 29.tmplfiles) plusassets/manifest.tomlmapping each destination to the profiles that get it. A file not in the manifest is never copied, and the generator exits 4 if the manifest and the tree disagree either way.
Profiles¶
Additive, so "agentic later" costs nothing now.
| Profile | Adds |
|---|---|
minimal |
package, tests, Justfile, CI, AGENTS.md |
cli (default) |
Tyro CLI, loguru, pydantic-settings, [project.scripts] |
lib |
publishing metadata, py.typed |
api |
FastAPI app with /docs and /openapi.json |
ml |
a marimo notebook that is also a batch CLI |
rust |
maturin backend, PyO3 crate, .pyi stubs |
The docs-drift gate¶
The piece worth stealing even if you use none of the rest. Prose about a CLI rots the first time someone renames a flag, so the generated project makes it a build artifact instead:
just docs-sync # rewrite the CLI block in AGENTS.md + .agents/skills/ from --help
just docs-check # exit 1 if the committed block is stale — part of `just check`
scripts/sync_agent_docs.py runs python -m <pkg>.cli --help plus each
subcommand's --help, strips ANSI, and splices the result between
<!-- BEGIN CLI --> markers. Because --check runs in the gate and in CI, a
flag rename that skips the docs fails the build.
Verified, not asserted¶
Every profile was generated and run before shipping: uv sync → just check
(ruff format, ruff lint, ty, pytest, docs-drift) green on all six, coverage
between 89% and 100%, and uv tool install → run → uninstall exercised
end-to-end. Two claims changed as a result of checking them:
- Tyro already ships shell completion. The first draft told you to wire up
shtab. It does not need it —
--tyro-write-completion {bash,zsh,tcsh,fish} PATHis built in, and--tyro-print-completionis deprecated because a strayprint()corrupts the output. uv syncsilently keeps a stale Rust binary. Editingrust/src/lib.rsand re-runninguv syncleaves the old.soin place with no warning — including any debug prints you just added. Confirmed by experiment;uv sync --reinstall-package <slug>is the fix.
Related¶
verifiable-surfaces ·
project-knowledge-harness ·
agent-history-hygiene ·
mkdocs-site-bootstrap ·
git-workflow ·
marimo-batch-mlflow ·
fastapi-ai-patterns ·
cli-release-distribution