copier-pyproject¶
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
docswheninclude_docsis on) and tox-uv runners across Python 3.11–3.14; builds viahatchling/hatch-vcswith versions derived from Git tags. - Type safety — full type hints and a
py.typedmarker, 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 thestyleenv (semgrep SAST), and a dependency-hygiene gate (deptry + creosote,tox -e depsand adepsCI 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-secretsgate with a committed.secrets.baseline(complementing gitleaks' history-spanning scan), GitHub Actions static analysis (zizmor + ghalint) enforcing least-privilegepermissions,persist-credentials: false, per-jobtimeout-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-onCITATION.cffwith 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 updatePR 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 anexamples/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_usergithub_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_nameauthor_emailcopyright_year(the yearLICENSEsays the copyright starts, rendered as<year>-present; defaults to the current year and is then kept bycopier update— see the LICENSE start year)short_descriptionpackage_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 getspkg run/pkg devlaunch verbs — see run/dev launch verbs)web_framework(fastapi/litestar - wheninclude_webis 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 - wheninclude_workeris enabled)include_database(include an async database layer: SQLAlchemy models, repositories and Alembic migrations via Advanced Alchemy; on in theweb/fullpresets — see the database layer)database_backend(sqlite/postgres - wheninclude_databaseis 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 toasynciowhen web/tui/mcp/worker/database is enabled,noneotherwise — 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 anexamples/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 wheninclude_docs:minorX.Y(default) /majorX/fullX.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 thecoverage-reportCI 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 aREPO_ADMIN_TOKENPAT)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 - wheninclude_redisis enabled, or when aredisworker broker is used even withinclude_redisdisabled)include_pgadmin(include pgAdmin - wheninclude_postgresis enabled)include_adminer(include Adminer - wheninclude_postgresis enabled)include_dbeaver(include CloudBeaver database UI in devcontainer)include_vpn(include OpenVPN sidecar in devcontainer)
Scaffold a project¶
- Install Copier and uv (e.g.,
uvx copier). - Run
copier copy https://github.com/hasansezertasan/copier-pyproject.git <destination>(orcopier copy . <destination>from a local clone). Use the.gitHTTPS URL, not thegh:hasansezertasan/copier-pyprojectshorthand — Copier records the argument verbatim as_src_path, and Renovate's copier manager only resolves template tags when_src_pathis a real git URL (see copier-manager updates). - Optionally seed answers with
.example-input.ymlusing--data-file .example-input.yml --defaults. - 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). - Open the generated README (rendered from
template/README.md.jinja) and clear theTODO @...markers inREADME.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:
- On push to
main, release-please opens a release PR derived from your Conventional Commits. - Merging that PR creates the git tag and a draft GitHub Release.
- The same workflow then builds with uv, publishes to PyPI via trusted publishing (unless
include_pypi_publishis 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. - Once the release is un-drafted, the workflow's
deploy-docsjob builds the Sphinx docs and publishes them withJamesIves/github-pages-deploy-action. Docs deploy inline here (rather than via arelease: publishedtrigger) because an event fired byGITHUB_TOKENcannot start another workflow;.github/workflows/gh-pages.ymlis 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):
- Open Trusted Publisher Management.
- Under "Add a new pending publisher", pick "GitHub".
- Set
PyPI Project Nameto your package name. - Set
Ownerto your GitHub username. - Set
Repository nameto your repo name. - Set
Workflow nametorelease.yml. The PyPI publish step lives in this workflow (not a reusable one), so this is the filename PyPI's OIDC check matches against. - Set
Environment nametopublish(or your chosen env). - 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):
- Open Codecov and add your repository.
- Copy the repository upload token from its Codecov settings.
- In your GitHub repository, add it as a repository secret named
CODECOV_TOKEN(Settings → Secrets and variables → Actions, orgh 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-docsjob pushes the built Sphinx docs to agh-pagesbranch. 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.jsonis 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):
- Open Personal access tokens in your Docker Hub account settings.
- 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. - Copy the token; it is shown only once.
- In your GitHub repository, go to Settings → Secrets and variables →
Actions (or use
gh secret set) and add two repository secrets: DOCKERHUB_USERNAME: Your Docker Hub usernameDOCKERHUB_TOKEN: The access token from step 3- 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:
- Land
feat:/fix:commits onmain. release-please opens (and keeps updating) a release PR with the computed version bump and changelog. - Merge the release PR when you want to ship. This creates the git tag and a draft GitHub Release.
- The
release.ymlworkflow then builds the wheel/sdist with uv, publishes to PyPI via trusted publishing, attaches artifacts to the draft, and un-drafts the release. - If
include_webis enabled it also builds and pushes multi-arch Docker images; if any ofinclude_launcher(PyCrucible),include_compiler(Nuitka), orinclude_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-actionafter the release is un-drafted (inline, since aGITHUB_TOKEN-firedrelease: publishedevent 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.