---
A modern Python package template with batteries included: strict type checking, automated linting, multi-stage Docker builds, MkDocs documentation, and a full CI/CD pipeline via GitHub Actions.
Powered by [Copier](https://copier.readthedocs.io) — scaffold a new project in seconds, and keep it up to date as the template evolves.
---
- Features
- Using this Template
- Working on the Template Itself
- Project Structure
- Development Workflow
- Running Tests
- Code Quality
- Documentation
- Docker
- CI/CD
- Publishing to PyPI
- Contributing
- License
Requirement: Copier 9.0 or newer. Install it once with:
pipx install copier # or uv tool install copier
Run the following command and answer the prompts:
copier copy gh:peinser/template-python <destination-directory>Copier will ask for:
| Variable | Description |
|---|---|
project_name |
Human-readable name (e.g. My Awesome Library) |
project_description |
One-line description of the project |
author_name |
Your full name |
author_email |
Your e-mail address |
repo_url |
Full HTTPS GitHub URL (e.g. https://github.com/owner/repo) |
module_name |
Python module name — lowercase, underscores only (e.g. my_library) |
After generation, git init and git add . are run automatically. Then:
cd <destination-directory>
uv sync --locked # install all dependencies
make help # see available tasksTip: The answers are stored in
.copier-answers.ymlinside the generated project. Do not delete this file — it is required for future updates.
When this template is updated, regenerate your project to pull in the latest changes:
cd <your-project>
copier updateCopier replays your original answers, applies the diff, and lets you resolve any conflicts. Review the changes with git diff before committing.
All variables are validated at prompt time:
author_emailmust match a basicuser@host.tldpattern.repo_urlmust start withhttps://github.com/.module_namemust be a valid Python identifier: starts with a lowercase letter, followed by lowercase letters, digits, or underscores.
The sections below describe the reference project bundled with this template. They also serve as living documentation for the generated project's README.md.
| Area | Tooling |
|---|---|
| Package manager | uv — fast, lock-file-based |
| Linting & formatting | Ruff |
| Type checking | MyPy (strict mode) |
| Testing | pytest + pytest-asyncio + pytest-cov |
| Security scanning | Bandit |
| Documentation | MkDocs Material with auto-generated API reference |
| Changelog | towncrier |
| Build system | Hatchling |
| Containerization | Multi-stage Docker build (validate → production) |
| CI/CD | GitHub Actions (docs, Docker image, PyPI publish) |
| Dev environment | VS Code Dev Container with act for local workflow testing |
For Option A (Dev Container):
- Docker (Desktop or Engine)
- VS Code with the Dev Containers extension
For Option B (local):
- Python 3.12 or newer
- uv
The dev container provides a fully configured, reproducible environment with all tools pre-installed (including act for local CI testing).
-
Clone the repository:
git clone https://github.com/peinser/template.git cd template -
Open in VS Code and reopen in container:
Ctrl+Shift+P → Dev Containers: Reopen in ContainerVS Code will build the container image and run the post-creation script, which installs all dependencies automatically via
uv sync --locked. -
Verify the setup:
make help
That's it. Skip to Development Workflow.
-
Clone the repository:
git clone https://github.com/peinser/template.git cd template -
Install uv (if not already installed):
curl -LsSf https://astral.sh/uv/install.sh | sh -
Install dependencies:
make setup
This runs
uv sync --locked, creating a virtual environment at.venv/and installing all pinned dependencies fromuv.lock. -
Verify the setup:
make help
template/
├── .changelog/ # Pending changelog entries (towncrier)
├── .dev/
│ └── compose.yml # Docker Compose for the dev container service
├── .devcontainer/ # VS Code dev container definition
├── .github/
│ ├── dependabot.yml # Automated dependency updates (weekly)
│ └── workflows/
│ ├── docs.yml # Build and deploy documentation to GitHub Pages
│ ├── image.yml # Build and push Docker image to registry
│ └── pypi.yml # Build and publish package to PyPI
├── docker/
│ ├── Dockerfile # Multi-stage build: builder → validate → production
│ └── entrypoint.sh # Container entrypoint
├── docs/ # MkDocs source
├── examples/ # Usage examples (add yours here)
├── src/
│ └── template/ # Package source code
│ ├── __init__.py
│ └── __version__.py
├── tests/ # pytest test suite
├── CHANGELOG.md # Auto-generated changelog
├── CONTRIBUTING.md # Contribution guidelines
├── Makefile # Common development tasks
├── mkdocs.yml # Documentation configuration
├── pyproject.toml # Project metadata, dependencies, and tool config
└── uv.lock # Locked dependency versions (do not edit manually)
All common tasks are available through make. Run make help to see the full list.
| Command | Description |
|---|---|
make setup |
Install all dependencies (first-time setup) |
make sync |
Re-sync dependencies after editing pyproject.toml |
make lock |
Update uv.lock after adding or removing dependencies |
make format |
Auto-format code with Ruff |
make lint |
Run Ruff (linter) and MyPy (type checker) |
make test |
Run the test suite with coverage |
make clean |
Remove build artefacts and caches |
make all |
Full local CI pipeline: clean → install → lint → test |
uv add <package> # runtime dependency
uv add --dev <package> # development-only dependency
make lock # update uv.lockmake testThis runs pytest with branch coverage enabled. A minimum of 75% coverage is required. To view a detailed HTML report:
uv run pytest --cov=src --cov-report=html
open htmlcov/index.htmlTests requiring async support use pytest-asyncio. Mark async test functions with @pytest.mark.asyncio.
make formatRuff reformats all source files in place (line length: 120).
make lintRuns two checks in sequence:
- Ruff — covers flake8, isort, pyupgrade, bugbear, and more.
- MyPy — strict type checking across
src/template/andtests/.
Both checks must pass before a pull request can be merged.
Bandit runs automatically in the Docker validate stage and in the image.yml workflow. To run it locally:
uv run bandit -r src/Documentation is written in Markdown and built with MkDocs Material. The API reference is generated automatically from docstrings.
uv run --group docs mkdocs serveOpen http://localhost:8000 in your browser. The site rebuilds on file changes.
uv run --group docs mkdocs buildOutput is written to site/.
Documentation is deployed to GitHub Pages automatically by the docs.yml workflow on every push to main that touches docs/, src/, or mkdocs.yml.
The Dockerfile uses a three-stage build:
| Stage | Purpose |
|---|---|
builder-base |
Installs locked dependencies (no dev extras) |
validate |
Runs format check, Ruff, MyPy, pytest, and Bandit |
production |
Minimal runtime image; runs as a non-root user (UID 1001) |
# Run only the validation stage
docker build --target validate -f docker/Dockerfile .
# Build the final production image
docker build -f docker/Dockerfile -t template:local .docker run --rm template:localThree GitHub Actions workflows handle the full pipeline:
| Workflow | Trigger | What it does |
|---|---|---|
docs.yml |
Push to main (docs/src/mkdocs) or manual |
Builds and publishes documentation to GitHub Pages |
image.yml |
Push to main/development (docker/src/tests) or manual |
Validates and builds the Docker image, pushes to registry |
pypi.yml |
Push to main or manual |
Builds and publishes the package to PyPI |
Dependabot checks for outdated GitHub Actions and Python dependencies weekly, grouping updates into a single PR.
The dev container ships with act, which lets you run GitHub Actions workflows locally before pushing.
-
Create a scoped Personal Access Token (PAT):
- GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens
- Restrict to this repository only
- Required permissions: Actions (read & write), Contents (read & write)
-
Export the token:
export GITHUB_TOKEN=ghp_your_token_here -
Run all workflows:
make act
-
Run a specific job:
make act-job JOB=<job-name>
Security: Store the PAT in a password manager. Never commit it to the repository. Revoke the token when it is no longer needed.
This project uses uv for building and publishing. Releases are published automatically via GitHub Actions using Trusted Publishing (OIDC), which is the most secure method in 2026 — no long-lived API tokens are required.
- Go to your repository → Settings → Environments → New environment.
- Name it
pypi(this name is conventional and works well with the workflow). - (Optional but recommended) Add protection rules:
- Require manual approval from specific people/teams (adds a safety gate before publishing).
- You can also restrict which branches or tags can deploy to this environment.
This environment provides an extra layer of control and clearly signals that the job is performing a production release.
You only need to do this once per package (or when you change the workflow name/repository).
-
Go to https://pypi.org/manage/account/publishing/ (or navigate to your project → Manage → Publishing).
-
Click Add a new trusted publisher.
-
Fill in the following details:
- Publisher:
GitHub - Repository owner: your GitHub username or organization (e.g.
peinser) - Repository name: the name of this repo (e.g.
template) - Workflow filename:
pypi.yml(or whatever you name the workflow file, e.g..github/workflows/pypi.yml) - Environment name:
pypi(must match the environment you created in GitHub)
- Publisher:
-
Save the trusted publisher.
Tip: You can add the same trusted publisher for TestPyPI if you want to test releases first.
The publishing workflow (.github/workflows/pypi.yml) should look similar to this (see the full recommended version in the repository):
- It uses
astral-sh/setup-uv@v7 - It builds with
uv build --frozen - It publishes with
uv publishusing Trusted Publishing (noPYPI_API_KEYsecret needed) - The job references the
pypienvironment and has the requiredid-token: writepermission
- Update the version in
pyproject.toml. - Commit and push to
main. - (Recommended) Create a GitHub Release with a tag like
v1.2.3— this is the cleanest trigger.- Or manually trigger the PyPI workflow from the Actions tab.
The workflow will:
- Build the source distribution and wheel
- Perform a quick smoke test
- Publish to PyPI (after manual approval if you enabled it)
- Trusted Publishing eliminates the need to store PyPI tokens as GitHub secrets.
- Using a dedicated
pypienvironment + manual approval. - The workflow only runs on protected branches/tags (adjust the
on:trigger as needed).
All renaming and substitution is handled automatically by Copier — see Using this Template.
After generation you may still want to manually adjust:
.github/workflows/image.yml— update the Docker registry path and image name.CODEOWNERS— replace the placeholder GitHub handle(s) with your own.docker/entrypoint.sh— add your application's startup logic.
See CONTRIBUTING.md for the full contribution guide, including how to set up a PAT for local CI testing with act.