This template provides a standardized Python project structure with sensible defaults, tailored to use in Python projects. It streamlines project setup and encourages best practices from the start.
The following tools must be installed:
- Python 3.12 or newer
- uv or pipx (if you want to install Copier in an isolated environment, accessible everywhere)
- copier
-
Install Copier (if not already):
pipx install copier
or
uv tool install copier
-
Generate files in your target repository (interactive):
copier copy /path/to/copier-template /path/to/your-repo
Replace
/path/to/copier-templatewith the path to this template, and/path/to/your-repowith your target repository.Python version: the template asks which Python version the project targets (
python_version, default3.12, choices are released CPython 3.12+ versions). The answer pinsmise.toml,requires-python, and Ruff'starget-version. Maintainers refresh the choices from endoflife.date withmise run update:python-versions.Note: If you're testing with unreleased template changes, use
--vcs-ref=HEAD:copier copy --vcs-ref=HEAD /path/to/copier-template /path/to/your-repo
-
Generate non-interactively using the provided sample data file: A sample data file is provided at
examples/config-basic.yml.copier copy --data-file examples/config-basic.yml /path/to/copier-template /path/to/your-repo
You can duplicate and modify that file to create variants (e.g.,
data-with-license.yml).Note: This template uses conditional tasks (see Tasks Behavior), which require the
--trustflag:copier copy --trust --data-file examples/config-basic.yml /path/to/copier-template /path/to/your-repo
-
(Optional) Override specific values at the CLI: You can still override one or more answers on the fly:
copier copy --trust --data-file examples/config-basic.yml -d project_name=override_name /path/to/template /dest
-
Review the generated files:
cd /path/to/your-repo git statusThe template runs
uv syncafter rendering and commits the result, includinguv.lock: anInit commitin a new directory, orchore: scaffold from copier templateinside an existing repository.
This template uses templated _exclude to manage optional features (LICENSE file, GitHub integration, src vs main.py, py.typed). Understanding their behavior is important for safe updates:
Exclusions are evaluated during rendering, before tasks run. This means:
- Optional files are never created when excluded
uv syncinstalls the project and generates or refreshesuv.lock_tasksrun after rendering:uv python install,uv sync,mise trust, thentasks/lifecycle.pyfor the Git commit andmise run installandmise run ci
When you generate a new project with conditions set:
- β
include_license=trueβ LICENSE file is rendered - β
include_license=falseβLICENSEis excluded, never created - β
github_integration=trueβ.github/workflow files are rendered - β
github_integration=falseβ.github/is excluded, never created
Tasks are executed again during updates. This is important:
- β
Safe with
--defaults(keeps your previous answers as-is) - β Safe if you keep boolean settings unchanged
β οΈ Dangerous if you change boolean settings when you have customizations
Recommended - Keeps all previous answers:
copier update --defaults --trust
# Your github_integration and include_license settings remain unchangedSafe if no customizations in affected directories:
copier update --trust --data github_integration=false
# OK if you haven't customized .github/ subdirectories# Initial generation WITH GitHub integration
copier copy --trust --vcs-ref=HEAD \
--data project_name=myproject \
--data github_integration=true \
/template /myproject
# Later: You customize workflows in .github/workflows/
# Then: Update but accidentally change github_integration to false
copier update --trust --data github_integration=false
# Result: π΄ excluded .github paths are removed, DELETING CUSTOM WORKFLOWS!If you need to disable GitHub integration after customizing .github/:
-
Manually back up your customizations:
cp -r .github/workflows /tmp/my-workflows-backup cp LICENSE /tmp/my-license-backup # if customized -
Run the update to disable features:
copier update --trust --data github_integration=false
-
Restore your customizations:
cp -r /tmp/my-workflows-backup /path/to/.github/workflows
-
Commit your changes:
git add .github/ git commit -m "Restore custom workflows"
| File | Purpose |
|---|---|
.copier-answers.yml |
Auto-maintained answers file for future updates (never edit manually). Now explicitly generated because the template includes templates/{{ _copier_conf.answers_file }}.jinja. |
examples/config-basic.yml |
Example data file passed with --data-file (excluded from rendered projects). |
Only templates/ is copied into generated projects (_subdirectory: templates in copier.yaml). Helper folders (examples/, tests/, docs/, scripts/, tasks/) stay outside templates/ so consumer projects stay clean. License partials under templates/licenses/ are excluded from copy via _exclude.
To create a new variant template from existing data file configuration:
copier copy --data-file /path/to/config-basic.yml /path/to/template /destFor advanced configuration (tasks, multiple templates) see the Copier docs sections: tasks, migrations, applying multiple templates.
You can edit the template files under templates/ (*.jinja plus verbatim payload like templates/prek.toml and templates/.github/workflows/*.yml) to fit your team's standards. See Copier documentation for advanced templating and options.
This template includes comprehensive validation tests to ensure generated projects meet expected standards.
# Run all validation tests
mise run testTests use session-scoped fixtures for optimal performance:
- Session-scoped registry:
projects.render("<variant>")renders each project once per test session, reused across all test modules - Tasks only where they add coverage: content-only variants render with
skip_tasks=True - All tests read-only: No mutations to generated projects ensures fixture reuse is safe
Test files are located in tests/; docs/architecture.md describes each module. Fixtures live in tests/conftest.py, and the render variants and shared loaders in tests/_projects.py.
All *.toml files (pyproject.toml, mise.toml, prek.toml, cliff.toml) are formatted with taplo (taplo = "0.10.0", pinned in mise.toml), enforced by the taplo-format prek hook β it autofixes on every commit, so there is no mise run task for it. Run it manually via mise run hooks, or check with mise x taplo -- taplo format --check <file>.
Style rule: one array item per line, always. Taplo has no force-expand switch (array_auto_collapse = false does not stick and trailing commas do not protect), so taplo.toml sets column_width = 1, which deterministically expands every array instead of collapsing the ones that fit (empty arrays stay [\n]; comments and key order are preserved β verified against taplo 0.10.0, see the comment in taplo.toml).
To discover available settings: mise x taplo -- taplo config schema prints the full JSON schema (formatting, include/exclude, per-path rule overrides). The Copier payload (templates/pyproject.toml.jinja, rendered mise.toml) emits taplo-clean output, so generated projects pass the hook with no extra step.
The prek.yml workflow keeps hook
versions fresh without manual effort.
| Aspect | Detail |
|---|---|
| Schedule | Monthly, 06:00 UTC on the 1st (0 6 1 * *) |
| Manual run | Actions tab β Scheduled Prek Auto-Update β Run workflow (workflow_dispatch) |
| Runner | ubuntu-latest with latest stable Python 3 (3.x floating spec, resolved on every run) |
| Permissions | contents: write so GITHUB_TOKEN can push the bump commit |
| Caching | ~/.cache/prek via one actions/cache step β hook envs are reused instead of rebuilt each run |
| Safety | timeout-minutes: 10; concurrency group serializes overlapping scheduled/manual runs |
| Commit | chore(deps): auto-update prek hooks via github-actions[bot], only when the config actually changed |
How it works:
- Checks out with
fetch-depth: 0(avoids shallow-clone push issues). - Runs
prek updateonprek.tomlβ the single config, shipped verbatim into projects generated from this Copier template. - Detects changes via
git diff --quiet(step outputchanges, not env vars); commits and pushes only when something changed β no empty commits.
Local testing is automated β see scripts/README.md.
Project-specific guides and workflows:
| Topic | Location |
|---|---|
| CI/CD Pipeline | docs/ci.md |
| Development Setup | docs/development.md |
| Template Architecture | docs/architecture.md |
For framework and tool documentation:
- Overview of Python Packaging
- Python classifiers: PyPI Classifiers
- Python Packaging: PEP 621 - pyproject.toml
- System Package Data Exchange: SPDX License List
- Writing your pyproject.toml
- Copier Framework: Copier Documentation
- Git Hooks: prek Documentation
- Ruff Formatter: Ruff Documentation
- TOML Formatter: taplo Documentation
- UV Package Manager: UV Documentation