Skip to content

copier-pyproject

Keep the Why · live

Copier template for a modern, typed Python package/CLI with uv, hatch, tox, and GitHub automation baked in.

Features

Always included in every generated project:

  • Packaging & workflow — uv-first with dependency groups (dev, style, test, tool, prek — plus docs when include_docs is on) and tox-uv runners across Python 3.11–3.14; builds via hatchling/hatch-vcs with versions derived from Git tags.
  • Type safety — full type hints and a py.typed marker, checked by mypy, basedpyright, ty, pyrefly, and zuban.
  • Code quality — ruff linting/formatting and an always-on pylint gate, plus vulture, slotscheck, taplo, validate-pyproject, typos, actionlint, editorconfig-checker, and import-linter architecture-contract enforcement, plus a semgrep SAST pass over src/ in the style env (semgrep SAST), and a dependency-hygiene gate (deptry + creosote, tox -e deps and a deps CI job) (dependency hygiene).
  • Testing — pytest with coverage/xdist/reruns (and .github/codecov.yml) and parallel execution.
  • CI/CD & release — matrix tests on macOS/Linux/Windows with per-component pytest markers and path-filtered CI jobs (unchanged components skip), trusted-publishing to PyPI, and release automation via release-please, with PR title linting, linked-issue enforcement, and a PR task-list completion check.
  • Security — CodeQL, OpenSSF Scorecard (with README badge), a dependency-review gate that blocks high-severity vulnerabilities, active scanning (gitleaks, pip-audit, and Trivy for web images), a local pre-commit detect-secrets gate with a committed .secrets.baseline (complementing gitleaks' history-spanning scan), GitHub Actions static analysis (zizmor + ghalint) enforcing least-privilege permissions, persist-credentials: false, per-job timeout-minutes, and full-length action SHA pins — a blocking prek/CI gate plus a zizmor Security-tab dashboard — and a CycloneDX SBOM attached to every release.
  • Repo hygiene — issue/PR templates, SECURITY.md, SUPPORT.md, CODEOWNERS, FUNDING, LICENSE, .gitattributes, .dockerignore, a badge-rich README, and VS Code launch configs (current file, tests, attach, entry points); always-on Commitizen and git hooks (run via prek) and an always-on CITATION.cff with a validation workflow.
  • Managed .gitignore — kept in sync with the upstream github/gitignore templates by cobo, with a weekly drift check.
  • Dependency & template updates — Renovate manages dependencies and, via its copier manager, opens a copier update PR whenever this template publishes a new tag (see copier-manager updates).
  • AI-agent onboarding — a concise AGENTS.md (the cross-tool standard), so coding agents share a single source of truth.
  • Modern Python — uv for dependency management, hatch for building, and a devcontainer for reproducible environments.

On by default, opt-out: a Sphinx documentation site (include_docs) — the Shibuya theme, autodoc API reference, GitHub Pages deployment, live per-PR previews, and a build-warning allowlist gate (docs/expected_warnings.txt + check_warnings.py). Published docs are versioned — each release deploys under its version slug (docs_version_granularity: minor X.Y, major X, or full X.Y.Z) with an in-page version switcher and a latest alias, and every page footer shows its git "last updated" date (see versioned docs). With a Typer CLI, a CLI reference page is generated at build time straight from the live app (typer ... utils docs) so it never drifts from --help. Turn it off for a README-only project; the maintainer setup guide (REPOSITORY_SETUP.rst) ships regardless.

Opt in per project (see Inputs for the full list): a Typer CLI, a FastAPI/Litestar web app (container-ready Dockerfile), a Tkinter GUI, a Textual TUI, an MCP server, a FastStream worker, Cython C extensions with multi-platform wheel building, profiling tools (py-spy, scalene, cProfile), CodSpeed-backed performance benchmarks, standalone-executable packaging (PyCrucible / Nuitka / PyInstaller), and extra quality integrations — SonarCloud, Sourcery, all-contributors, smokeshow (a tokenless, account-free coverage-HTML mirror for public repos), MegaLinter (adds gap checks — shellcheck, hadolint, jsonlint, jscpd clone-detection, and a .md-scoped cspell prose pass — not already covered by prek/tox), and repository-settings-as-code (a .github/settings.yml syncing description/homepage/topics via the "Settings" GitHub App).

Inputs

Preset

The first component question, preset, seeds sensible defaults matching your project's shape:

  • library — a pure importable package (seeds an examples/ folder). Default.
  • tool — a CLI/TUI application distributed on PyPI (CLI + TUI + pydantic-settings).
  • web — a web app with a database and cache (web app + pydantic-settings + PostgreSQL + Redis in the devcontainer).
  • full — every component and integration enabled.

The preset only changes each toggle's default (which you can accept with Enter) — it never hides a question and never changes an existing project on copier update; every toggle is still written to .copier-answers.yml. A few sub-questions (the web framework, worker broker, Redis backend, and the PostgreSQL UIs) remain conditional on their parent toggle, independently of the preset.

Copier will prompt for:

  • github_user
  • github_repo_name (valid Python package name: lowercase letters/digits/underscores, starts with a letter, and not a Python reserved keyword — used verbatim as the import package name, so no dashes)
  • author_full_name
  • author_email
  • copyright_year (the year LICENSE says the copyright starts, rendered as <year>-present; defaults to the current year and is then kept by copier update — see the LICENSE start year)
  • short_description
  • package_keywords (extra comma-separated PyPI keywords; tooling/component keywords are added automatically)
  • include_cli (include Typer CLI)
  • include_web (include web API; its OpenAPI schema is generated into the docs, and the project gets pkg run / pkg dev launch verbs — see run/dev launch verbs)
  • web_framework (fastapi/litestar - when include_web is enabled)
  • include_gui (include Tkinter GUI)
  • include_tui (include Textual TUI)
  • include_mcp (include MCP server support)
  • include_worker (include message queue worker using FastStream; its AsyncAPI message-interface schema is generated into the docs)
  • worker_broker (kafka/nats/rabbitmq/redis - when include_worker is enabled)
  • include_database (include an async database layer: SQLAlchemy models, repositories and Alembic migrations via Advanced Alchemy; on in the web/full presets — see the database layer)
  • database_backend (sqlite/postgres - when include_database is enabled; defaults to postgres where the preset enables the devcontainer Postgres service — see backend alignment)
  • async_style (async test runner for generated tests: none / asyncio / anyio; defaults to asyncio when web/tui/mcp/worker/database is enabled, none otherwise — see async testing style)
  • include_c_extensions (include C extensions support using Cython)
  • include_profiling (include profiling and performance tools)
  • include_benchmarks (include pytest-codspeed benchmarks and a non-blocking CodSpeed CI workflow)
  • include_examples (include an examples/ folder with simple and advanced usage stubs)
  • include_docs (Sphinx docs site — docs/ tree, docs-* tox envs, docs CI + versioned Pages deploy + version switcher; on by default)
  • docs_version_granularity (asked when include_docs: minor X.Y (default) / major X / full X.Y.Z — the per-release docs directory slug)
  • include_launcher (uv-bootstrap launcher via PyCrucible — small executable, downloads Python+deps on first run)
  • include_compiler (compiled native executable via Nuitka — source compiled to machine code)
  • include_freezer (offline freezer via PyInstaller — self-contained bundle, no Python on target)
  • include_pypi_publish (publish to PyPI via trusted publishing on each release; on by default — turn it off for a private project, a taken PyPI name, or GitHub-Releases-only distribution, see optional PyPI publishing)
  • include_pydantic_settings (use pydantic-settings for configuration; the docs build auto-generates a Configuration reference from the live settings model via autodoc-pydantic)
  • include_megalinter (opt-in extra CI quality layer; runs gap linters — shellcheck, hadolint, jsonlint, jscpd, and a .md-scoped cspell — not covered by prek/tox)
  • include_smokeshow (opt-in tokenless coverage-HTML host; publishes the combined report to an ephemeral public URL from the coverage-report CI job — public repos only, no account or secret)
  • include_repo_ruleset (opt-in branch protection as code — a ruleset + App-free sync workflow enforcing squash-only merges, linear history, and the required CI checks; needs a REPO_ADMIN_TOKEN PAT)
  • include_repo_settings (repository metadata via the Settings App; installation requires a distinct trusted reviewer and verified code-owner review, so solo maintainers defer the App)
  • include_postgres (include PostgreSQL service in devcontainer)
  • include_redis (include Redis/Valkey service in devcontainer)
  • redis_backend (redis/valkey - when include_redis is enabled, or when a redis worker broker is used even with include_redis disabled)
  • include_pgadmin (include pgAdmin - when include_postgres is enabled)
  • include_adminer (include Adminer - when include_postgres is enabled)
  • include_dbeaver (include CloudBeaver database UI in devcontainer)
  • include_vpn (include OpenVPN sidecar in devcontainer)

Scaffold a project

  1. Install Copier and uv (e.g., uvx copier).
  2. Run copier copy https://github.com/hasansezertasan/copier-pyproject.git <destination> (or copier copy . <destination> from a local clone). Use the .git HTTPS URL, not the gh:hasansezertasan/copier-pyproject shorthand — Copier records the argument verbatim as _src_path, and Renovate's copier manager only resolves template tags when _src_path is a real git URL (see copier-manager updates).
  3. Optionally seed answers with .example-input.yml using --data-file .example-input.yml --defaults.
  4. Initialize git in the destination: cd <destination> && git init (the template intentionally defines no Copier tasks, so it does not auto-init — see no Copier tasks).
  5. Open the generated README (rendered from template/README.md.jinja) and clear the TODO @... markers in README.md, pyproject.toml, docs, and workflows.

Claude Code instruction loading

The default generated project uses AGENTS.md without a CLAUDE.md wrapper. Direct loading requires Claude Code v2.1.277 or later; use v2.1.281 or later for sessions such as Amazon Bedrock or those with telemetry disabled. By default, a CLAUDE.md, .claude/CLAUDE.md, or CLAUDE.local.md in the working directory or an ancestor prevents automatic AGENTS.md loading. Your global ~/.claude/CLAUDE.md does not prevent it.

If you keep one of those project or ancestor files, open /config and set Project instructions to claude-md-and-agents-md to load both. For an older client or a session with AGENTS.md support disabled, add a local CLAUDE.md containing @AGENTS.md instead. See Claude Code's instruction-loading documentation for the full rules. For per-host agent and skill files beyond AGENTS.md, use copier-skill-repo.

A project first scaffolded from v1.6.0 or earlier carries a CLAUDE.md wrapper (v1.7.0 stopped rendering it but kept existing copies). copier update now deletes it, customized or not. Before updating, move any project-specific guidance from it into AGENTS.md.

Adopt into an existing project

The copier copy flow above is for new projects. Adopting this template into an existing or already-published package is a different job: copier copy overwrites source, config, docs, and CI, so it must be run as a migrate-and-reconcile rather than a scaffold.

Generated projects carry the adoption guidance in their REPOSITORY_SETUP.rst: its Template updates section covers later copier update runs, and its final Adopting into an existing project section covers the baseline, the hazards, check-coverage and manifest parity, and validation. Generated projects carry no other indication of how they were created beyond .copier-answers.yml and the README.

Before scaffolding an existing package

Give your agent this prompt in the package's repository:

Adopt https://github.com/hasansezertasan/copier-pyproject.git into this package.
Use the latest released template version and record its exact tag. First inspect
the repository and agree with me on explicit --data answers that preserve its
dependencies, Python support and interfaces. If the latest release is incompatible,
explain why and agree with me on an alternative.
The target must ship an "Adopting into an existing project" section in its
REPOSITORY_SETUP.rst; if no compatible version does, stop before copying and
explain the limitation.
Require a clean working tree with all project work committed,
create a dedicated chore/adopt-copier-pyproject branch, and record the baseline commit
and last published version. If .copier-answers.yml already exists, use the
update flow instead of a new overwrite copy.

For first adoption, use uvx copier copy --overwrite --defaults with the .git URL,
--vcs-ref=<recorded-tag>, the agreed --data answers, and . as the destination.
Before installing
dependencies, running hooks or committing the output, read the "Adopting into an
existing project" section of the rendered REPOSITORY_SETUP.rst directly and
follow it to reconcile against the baseline. Preserve our real source, tests,
identity, release history and custom tooling; ask before consequential changes.
Then walk the rest of REPOSITORY_SETUP.rst.
Report validation results and any remaining setup steps.

After scaffolding

Copier prints a prompt for your agent. Give it:

Read the "Adopting into an existing project" section of REPOSITORY_SETUP.rst and
audit this scaffold against our existing behavior before running tools, then walk
the rest of REPOSITORY_SETUP.rst.

For a brand-new project, walk the setup guide in REPOSITORY_SETUP.rst after the quickstart; there is no existing code to reconcile.

The companion plugin and its adopt, update, and setup skills are retired. If you installed them previously, remove that installation and use the generated project's REPOSITORY_SETUP.rst. No plugin install is needed.

Generated project quickstart

  • Install dependencies: uv sync
  • Style gate: uv run --locked tox run -e style
  • Full test suite: uv run --locked tox run
  • Run the CLI (if included): uv run --locked <repo-name> version
  • Run the web app (if included): uv run --locked <repo-name>-web
  • Serve docs locally: uv run --locked tox run -e docs-server (deploys via GitHub Pages on release)

.example-input.yml supplies the required identity answers plus a starting preset (library), so the template can be rendered non-interactively (e.g. mise run example); every other toggle follows from the preset's defaults.

Working on the template

Every render goes through one entrypoint, tools/render.py (single render entrypoint) — the test harness, CI, the mise tasks and the generated docs all call it:

  • Render once: uv run tools/render.py render /tmp/x --data preset=full
  • Render the example project: mise run example
  • Re-render on every save while editing: mise run watch
  • Render tests: mise run test (golden files: mise run test-golden-update)
  • Rewrite the committed derived artifacts: mise run regenerate

docs/generated-project-trees.md is generated by that last command — the exact file set each preset produces, so the documented structure cannot drift from what the template actually renders.

Release automation

Release automation is standardized on release-please (see release-please as the sole orchestrator). A single unified workflow, .github/workflows/release.yml, orchestrates the whole release:

  1. On push to main, release-please opens a release PR derived from your Conventional Commits.
  2. Merging that PR creates the git tag and a draft GitHub Release.
  3. The same workflow then builds with uv, publishes to PyPI via trusted publishing (unless include_pypi_publish is off — then the release ships on GitHub Releases only), attaches the build artifacts to the draft, and only then un-drafts the release — so the release is never visible without its artifacts.
  4. Once the release is un-drafted, the workflow's deploy-docs job builds the Sphinx docs and publishes them with JamesIves/github-pages-deploy-action. Docs deploy inline here (rather than via a release: published trigger) because an event fired by GITHUB_TOKEN cannot start another workflow; .github/workflows/gh-pages.yml is kept for manual redeploys only.

CI runs on macOS/Linux/Windows via .github/workflows/ci.yml.jinja.

Versions are derived from git tags by hatch-vcs (dynamic = ["version"]), so release-please never edits a static version literal and uv.lock cannot desync. bump-minor-pre-major keeps pre-1.0 projects pre-1.0.

Pause merges that change .github/workflows/ (including Renovate action-pin updates) from merging a release PR until the entire Release run finishes. With GITHUB_TOKEN, workflow differences between the release commit and current main can block tag or release creation with a 403. The generated repository setup guide explains how to restore matching workflows, recover the release, and re-apply the changes. See #332.

PyPI Trusted Publishing setup

Enable PyPI once per project for .github/workflows/release.yml (skip this when include_pypi_publish is off):

  1. Open Trusted Publisher Management.
  2. Under "Add a new pending publisher", pick "GitHub".
  3. Set PyPI Project Name to your package name.
  4. Set Owner to your GitHub username.
  5. Set Repository name to your repo name.
  6. Set Workflow name to release.yml. The PyPI publish step lives in this workflow (not a reusable one), so this is the filename PyPI's OIDC check matches against.
  7. Set Environment name to publish (or your chosen env).
  8. Save.

Coverage reporting setup

CI uploads coverage to Codecov after the test suite runs, integrated in .github/workflows/ci.yml. On a public repository this needs no setup — the codecov-action uploads tokenless, so owner pushes and fork PRs both report coverage out of the box.

A CODECOV_TOKEN is only required for a private repository (or to avoid tokenless rate-limits):

  1. Open Codecov and add your repository.
  2. Copy the repository upload token from its Codecov settings.
  3. In your GitHub repository, add it as a repository secret named CODECOV_TOKEN (Settings → Secrets and variables → Actions, or gh secret set CODECOV_TOKEN).

The upload is best-effort either way: on a private repo with no token, CI records a notice and skips the upload rather than failing the run. The generated CONTRIBUTING.md documents the same setup for contributors to your project.

CI is split per component: a changes (path-filter) job lets each test-<component> job skip when its tree is untouched, and each component's coverage is gated fail_under = 99 scoped to its own subtree (combined across that component's OS cells). A central coverage-report job merges whatever ran and performs the single Codecov upload. Enabling include_smokeshow additionally publishes the combined HTML report to a tokenless ephemeral URL (public repos only) — a browsable coverage report with no account. Locally, pytest -m <component> selects one component's tests; the default suite still runs everything minus integration.

Documentation site and dependency updates

Two more one-time steps are needed after generating a project (both are documented in full, with copy-paste commands, in the generated CONTRIBUTING.md repository-setup section):

  • GitHub Pages — on release, the deploy-docs job pushes the built Sphinx docs to a gh-pages branch. Enable Pages once (Source → Deploy from a branch → gh-pages / root) after the first release creates the branch, otherwise the docs build but are never served.
  • Renovate — .github/renovate.json is read by the hosted Renovate app, which must be installed on the repository once; until then dependency-update PRs never open.

Container image publishing (if include_web is enabled)

On release, the workflow always publishes multi-arch images (amd64/arm64) to the GitHub Container Registry (ghcr.io/<owner>/<repo>) using the built-in GITHUB_TOKEN — no extra setup required.

Docker Hub publishing is optional, opted into via a pair of repository secrets (integrated in .github/workflows/release.yml):

  1. Open Personal access tokens in your Docker Hub account settings.
  2. Click "Generate new token", give it a recognizable description (e.g. <repo> release workflow), set an expiration date, and select the Read & Write access permission — Write is required to push; do not grant more.
  3. Copy the token; it is shown only once.
  4. In your GitHub repository, go to Settings → Secrets and variables → Actions (or use gh secret set) and add two repository secrets:
  5. DOCKERHUB_USERNAME: Your Docker Hub username
  6. DOCKERHUB_TOKEN: The access token from step 3
  7. On release, the workflow will build and push multi-arch images (amd64/arm64) to docker.io/<DOCKERHUB_USERNAME>/<repo> alongside GHCR. The namespace follows the Docker Hub account, so it does not need to match the GitHub owner.

The secret pair is all-or-nothing: with neither set, the workflow skips Docker Hub with a notice and publishes to GHCR only; with exactly one set, a preflight job fails fast and blocks every publish channel (PyPI included) so a misconfiguration can never produce a partial release. The generated CONTRIBUTING.md documents the same setup for contributors to your project.

Release steps

Releases are driven by Conventional Commits — you do not draft releases by hand:

  1. Land feat: / fix: commits on main. release-please opens (and keeps updating) a release PR with the computed version bump and changelog.
  2. Merge the release PR when you want to ship. This creates the git tag and a draft GitHub Release.
  3. The release.yml workflow then builds the wheel/sdist with uv, publishes to PyPI via trusted publishing, attaches artifacts to the draft, and un-drafts the release.
  4. If include_web is enabled it also builds and pushes multi-arch Docker images; if any of include_launcher (PyCrucible), include_compiler (Nuitka), or include_freezer (PyInstaller) is enabled it also builds standalone executables for Windows, macOS, and Linux and attaches them to the release.

Release workflow structure

The unified release.yml orchestrates every release job; all jobs after release-please run only when a release was created:

release-please ─► build ─┬─► pypi-publish ────────┐
                         ├─► build-launcher ──────┤
                         ├─► build-freezer ───────┼─► attach-github-release ─┐
                         ├─► build-compiler ──────┘                          │
                         │   (if launcher / freezer / compiler)              ▼
                         └─► docker-publish ───────────────────────────────► finalize-release        ► deploy-docs
                             (if web)                                        (un-draft + reconcile)    (sphinx-build + pages-deploy)
  • release-please: Opens/maintains the release PR; on merge, tags and creates the draft release
  • build: Builds the Python wheel/sdist with uv (matrix per-OS when include_c_extensions)
  • pypi-publish: Publishes to PyPI via trusted publishing
  • build-launcher / build-freezer / build-compiler: Build standalone executables for 3 platforms via PyCrucible / PyInstaller / Nuitka respectively (each conditional and independent)
  • docker-publish: Builds and pushes multi-arch Docker images (conditional)
  • attach-github-release: Attaches all artifacts to the still-draft release
  • finalize-release: Un-drafts the release and reconciles the next release PR
  • deploy-docs: Builds the Sphinx docs and publishes them with JamesIves/github-pages-deploy-action after the release is un-drafted (inline, since a GITHUB_TOKEN-fired release: published event can't trigger a separate workflow)

Author

This project is maintained by Hasan Sezer Taşan, It's me :wave:

Disclaimer

This template is not intended to be used for malicious purposes. The author is not responsible for any damage caused by this template. Use at your own risk.

License

This project is licensed under the MIT License - see the LICENSE file for details.