A home for your Python services and the code they share.
Start with a service, add a shared library, and keep them together in one repo.
Dependency management, tests, formatting, and Docker builds are ready to go.
Quick start · Everyday workflow · Project map · Docker · Migration
- 🐍 Modern Python. Python 3.14 by default, with CI checks for 3.12–3.15.
- 🗂️ One repo, many projects. Keep your services and shared libraries together.
- ⚡ Fast setup with uv. Manage dependencies and virtual environments in one place.
- 🔒 One shared lockfile. Give everyone the same dependency versions.
- 🚀 One-command scaffolding. Create a service or library with source, tests, and docs.
- 🧩 Easy code sharing. Use local libraries without rebuilding them after every edit.
- ✨ Clean code with Ruff. Format your Python and catch lint errors.
- 🔎 Type checks with ty. Find type errors while you work.
- 🧪 Tests with pytest. Run each project's tests independently.
- 🪝 Optional commit hooks. Run checks before your changes leave your laptop.
- 🐳 Docker included. Build service images with a non-root production runtime.
- 📦 Ready to package. Build wheels with type markers and license metadata.
- 🌍 Cross-platform smoke tests. Check generated projects on Linux, macOS, and Windows.
- 🤖 Dependency updates. Get weekly Dependabot PRs for Python, Actions, and Docker.
- 🛠️ Commands you'll remember. Use Make shortcuts or run uv directly.
Let's get your first service running. 🚀
Create a repo from this template,
clone it, and open a terminal at its root. You'll need
uv 0.11+, GNU Make, and
Python 3.12+ available as python3. See Working without Make
for direct commands, including on Windows.
# Set up the workspace (uv downloads Python 3.14 if needed)
uv sync --all-packages --locked
# Create a library and a service that can use it
make new-package name=shared-utils
make new-service name=my-service
uv add --package my-service --workspace shared-utils
uv sync --all-packages --locked
# Say hello
make -C services/my-service runHello from my-service!
Your library imports as shared_utils. Start adding code, then run make check
to check lint, types, tests, and formatting. Commit your new projects along with
uv.lock so everyone gets the same dependencies.
A few things to know about new projects
Names start with a lowercase letter and may contain lowercase letters, digits,
hyphens, or underscores. shared-utils becomes packages/shared-utils with an
import name of shared_utils. Names must be unique across services and libraries;
Python keywords and standard-library module names are rejected.
The generator works offline. After creating a project, run uv lock and
uv sync --all-packages --locked (adding a dependency with uv add also updates
the lockfile). Each project uses a src/ layout and includes license metadata
and a py.typed marker for tools that use its type annotations.
Run these at the repo root, or use make -C <project> for a single project.
| Command | What it does |
|---|---|
make install |
Install all projects and the locked development tools |
make format |
Format your Python code |
make check |
Run lint, types, tests, and a formatting check |
make lint / make typecheck / make test |
Run just one kind of check |
make -C packages/shared-utils test |
Test a single project |
make smoke |
Generate sample projects, build wheels, and test a fresh install |
uv build --package shared-utils |
Build a library wheel and source archive |
uv run --locked pre-commit install |
Enable commit hooks |
uv lock --upgrade |
Update dependencies for review |
Use make help to see the available commands. You can pass arguments too:
make -C services/my-service test args="-q".
uv add --package my-service httpx # For one service
uv add --package my-service --workspace shared-utils # A library from this repo
uv add --dev pytest-cov # A development tool
uv sync --all-packages --lockedLocal libraries are installed in editable mode, so your service can pick up source changes without rebuilding a wheel. All projects share one dependency resolution and need compatible requirements; use separate environments when they don't.
flowchart LR
subgraph workspace["Your repo · one uv workspace"]
service["services/my-service<br/>Runnable Python service"]
library["packages/shared-utils<br/>Reusable Python library"]
service -->|depends on| library
end
service -->|build| image["Docker image<br/>Production dependencies"]
.
├── packages/ Shared libraries, each with src/ and tests/
├── services/ Runnable services, each with a Dockerfile
├── templates/ Starting points for new projects
├── scripts/ Scaffolding, test runner, and smoke checks
├── tests/ Tests for the scaffolder
├── .github/ CI and Dependabot configuration
├── doc/ Your project documentation
├── pyproject.toml Workspace and tool configuration
├── uv.lock Shared dependency lockfile
└── .python-version Default Python version
Templates aren't installed as workspace projects. Tests run separately per project, so matching test filenames won't clash. Ruff settings come from the root config.
Your service gets a Dockerfile too. 🐳 After updating the lockfile, build and run it:
make -C services/my-service docker-build
make -C services/my-service docker-runBuilds use the repo root as their context, including any shared libraries your service needs. The runtime image contains the service and its production dependencies and runs as user 10001.
Keep secrets outside the build context; .dockerignore excludes environment files,
Git data, and local caches. Behind an HTTPS proxy, pass a combined CA bundle with
args="--secret id=proxy_ca,src=/path/to/ca.pem". Update the pinned uv versions in
Docker and CI together.
Pull requests and pushes to main check locked installation, lint, types,
formatting, and tests on Python 3.12–3.15.
Smoke tests on Linux, macOS, and Windows create connected projects, run their
checks, build wheels, and install them in a fresh environment. Linux also builds
and runs the Docker image. Try it locally with make smoke, or
make smoke args="--docker" to include Docker. These checks need network access;
the Docker check also needs Docker running.
A manual workflow lets you check one existing service or library. Dependabot opens weekly updates for Python dependencies, GitHub Actions, and Docker images.
Use uv directly on Windows, macOS, or Linux
uv sync --all-packages --locked
uv run --locked python scripts/scaffold.py service my-service
uv lock
uv sync --all-packages --locked
uv run --all-packages --locked python scripts/test_all.py
uv run --all-packages --locked ty check
uv run --locked ruff check .
uv run --locked ruff format --check .
uv run --locked python scripts/smoke_test.pyFor a library, replace service my-service with package shared-utils.
The scaffolder's own tests also run without installing development tools:
python -m unittest discover -s tests -v with Python 3.12+.
Updating an older Poetry-based copy
- Move Poetry metadata to
[project]and development dependencies to[dependency-groups]. - Add local workspace dependencies with
uv add --workspace. - Run
uv lock,uv sync --all-packages, andmake check. - Once you've verified the conversion, remove the Poetry lockfiles and commit
uv.lock. - Update service launch commands: services now use
src/<module>/__main__.pyand[project.scripts]instead ofsrc/run.py.
Ruff replaces Black, so expect a one-time formatting diff. Commit changes to
.python-version when changing the default Python version.
uv · Ruff · ty · pytest · Hatchling · pre-commit
Based on uv's workspace and Docker guides, with uv-docker-example and Substrate as references.
MIT licensed · Make yourself at home.