Scaffold a Python project
Creates a Python repository with the src/ layout, uv-managed dependencies, pinned developer
tooling, and a Makefile gate. Apply the python-style, python-typing, and python-testing
skills while filling in real code, and wire releases with the python-release skill.
1. Initialize with uv
uv is the only tool for interpreters, virtual environments, dependencies, locking, building, and
publishing — do not reach for pip, pipenv, poetry, conda, pyenv, or pipx alongside it.
uv init --package myapp # packaged app with a CLI entry point and [build-system]
uv init --lib myapp # importable library (same src/ layout, no console script)
Both flavors produce the src/ layout and a [build-system]; the bare uv init (flat, no build
system) is only for throwaway scripts. uv writes pyproject.toml and src/myapp/, and the first
uv sync/uv run writes the uv.lock you commit.
2. Lay out the tree
pyproject.toml
uv.lock # committed — the reproducible, cross-platform resolution
.python-version # pins the interpreter uv provisions for `uv run`
README.md # referenced by `readme` in pyproject.toml
src/myapp/__init__.py
src/myapp/__main__.py
tests/
Makefile
- src/ layout, never flat. Tests run against the installed package, so a missing
__init__.pyor a packaging mistake fails locally instead of being masked by an import-from-cwd. Copy templates/python-version to.python-version. - One module per concern, named for what it provides (
store,client,config) — neverutils,common, orhelpers.
3. Configure pyproject.toml on the uv_build backend
Copy templates/pyproject.toml. The choices that matter:
[build-system]uses uv's native backend —requires = ["uv_build>=0.11.21,<0.12"],build-backend = "uv_build". It is fast and needs no configuration for the src/ layout; the upper bound keeps builds reproducible as the backend evolves.requires-pythonis the floor (>=3.13);.python-versionpins the interpreter used during development. Runtime dependencies use lower bounds (>=), never==—uv.lockdoes the exact pinning.- The
[tool.ruff],[tool.ty.environment](or[tool.pyright]), and[tool.pytest.ini_options]blocks ship in the same file; their rationale lives in thepython-style,python-typing, andpython-testingskills.
4. Pin developer tooling in a dev dependency group
uv add --dev ruff ty pytest hypothesis
This lands in [dependency-groups] dev (PEP 735) — not [project.optional-dependencies].
Dependency groups are for developing the project and are never published in the wheel; optional
dependencies are runtime extras for consumers. uv sync installs the group; uv run <tool>
executes it from the project environment, so there are no global installs to drift. Swap ty for
pyright (uv add --dev pyright) if the project type-checks with pyright instead — see the
python-typing skill.
5. Create the Makefile gate
Copy templates/Makefile. Its pr target runs the full local gate in order —
fmt lint typecheck test build — every tool through uv run so the locked versions are used. It
mirrors the CI workflow (see python-release) so a green make pr means a green CI.
6. Keep the entry point thin
Copy the __main__.py template. main(argv) returns an int exit code and
raise SystemExit(main()) runs under if __name__ == "__main__"; the real work lives in
importable modules so tests call functions directly instead of shelling out. Register the console
script under [project.scripts].
7. Finish
- Write the first module and its tests (
python-style,python-typing,python-testingskills). - Document modules and public APIs as you go (
python-docsskill). - Wire releases, CI, and Renovate with the
python-releaseskill. - Run
make prand make sure it passes before committing.