A copier template for Python packages, built
from the conventions of lair,
fips, PYSTILT,
arl-met and friends. Unlike a one-shot
generator, copier records which template version a project came from, so
copier update brings later template improvements into existing projects as
an ordinary, reviewable diff.
uv tool install copier # or run it ad hoc: uvx copier ...
copier copy gh:jmineau/python-template my-packageCopier asks a few questions and prints the next steps (uv lock, git init,
uv sync, uv run pre-commit install). Later, from inside the project:
copier update # pull in template changes since your last copy/updateCopier merges the template's changes with yours; review the diff (conflicts get git-style markers) and commit it like any other change.
| Question | Default | Notes |
|---|---|---|
full_name, email, github_username |
James Mineau | Author and repository owner |
orcid, affiliation |
James's | For CITATION.cff and Zenodo; may be blank |
project_name |
— | Human-readable, e.g. "My Package" |
repository_name |
from project_name |
my-package |
distribution_name |
from repository_name |
The pip install / PyPI name |
package_name |
from distribution_name |
The import name (mypackage) |
project_short_description |
— | One line |
python_min |
3.11 | Oldest supported Python; CI tests it through 3.14 |
publish_to_pypi |
yes | Adds trusted publishing to PyPI on release |
docs_notebooks |
no | Runs notebooks, and code cells in Markdown pages, when the docs build (myst-nb); adds Jupyter to the dev tools |
test_os |
Linux, macOS, Windows | Where the tests run; Linux is required (coverage uploads from it) |
copyright_year |
2026 | LICENSE |
Copier can't merge a template into a project that didn't start from it, so the
first time is by hand; copier update takes over from then on.
-
Render the template with the package's answers into a scratch folder, and compare it with the package:
copier copy --defaults --data project_name=mypkg --data publish_to_pypi=true ... \ gh:jmineau/python-template /tmp/mypkg-baked diff -ru /tmp/mypkg-baked mypkg -
Port what applies, and keep what is the package's own (a compiled extension, extra CI jobs, a release schedule). One commit per concern reads best: versions from tags (setuptools-scm: drop the static version and its copies, and CITATION.cff's
version), the type checker, the Python floor, the template's tooling, docstring style. -
Copy the rendered
.copier-answers.ymlinto the package, then check thatcopier update --defaultsin a clean clone changes nothing. -
Expect warnings-as-errors to find things. Fix the package's own warnings; a test that triggers one on purpose asserts it with
pytest.warns; a third-party one is ignored by message and module in[tool.pytest]. -
Seed the docs site with the latest release, or the site opens at
dev/until the next tag. Build the release's docs from its tag, add them to thegh-pagesbranch with the template's script, and push it:git worktree add --detach ../mypkg-docs vX.Y.Z (cd ../mypkg-docs && uv sync --frozen && uv run sphinx-build -M html docs docs/_build) git clone --single-branch --branch gh-pages https://github.com/OWNER/mypkg site # or, if none yet: # git init site && git -C site checkout --orphan gh-pages uv run --no-project --with packaging python mypkg/.github/scripts/docs_versions.py \ site ../mypkg-docs/docs/_build/html X.Y.Z --base-url https://OWNER.github.io/mypkg/ git -C site add --all && git -C site commit -m "docs: publish X.Y.Z" && git -C site push origin gh-pages
Without a seed, the Documentation workflow creates the branch on its first run on main.
-
On GitHub, once the
gh-pagesbranch exists: Settings > Pages > Source: Deploy from a branch, branchgh-pages, folder/ (root); orecho '{"build_type": "legacy", "source": {"branch": "gh-pages", "path": "/"}}' | gh api -X PUT repos/OWNER/mypkg/pages --input -
GitHub Pages then rebuilds the site after every push to the branch.
| Area | Tooling |
|---|---|
| Environment | uv with a committed uv.lock; dev tools and docs in one PEP 735 dev group |
| Tasks | just: just quality-check, just test, just build-docs, just release X.Y.Z, ... CI runs the same recipes |
| Packaging | setuptools, src/ layout, PEP 639 license, py.typed |
| Versions | setuptools-scm: the version is the git tag; .devN versions between releases |
| Lint and format | ruff (E F UP B SIM I D D213 NPY RUF100; pydocstyle with the NumPy convention) |
| Types | pyrefly |
| Tests | pytest 9 (strict, warnings are errors, network/slow markers), parallel with pytest-xdist, pytest-cov, Codecov |
| Docstrings | NumPy style, docstr-coverage ≥ 95% |
| Docs | Sphinx + PyData theme, pandas-style API pages (a page per class, with tables of its attributes and methods, and a page per member), copy buttons, examples that run when the docs build (matplotlib's .. plot:: in docstrings, IPython's .. ipython:: in .rst pages, and with docs_notebooks, notebooks and code cells in Markdown pages), live preview (just docs-serve), built with warnings as errors; versioned on GitHub Pages (dev/, one folder per release, stable/) with a version dropdown |
| pre-commit | pre-commit-hooks, rST checks, validate-pyproject, zizmor; uv lock, ruff and pyrefly run from .venv, so uv.lock pins their versions |
| Changelog | Keep a Changelog, hand-edited; just changelog drafts entries from Conventional Commits (git-cliff) |
| CI | Tests (the chosen operating systems × all supported Pythons), Code Quality, Documentation, Publish (tag push → PyPI trusted publishing with attestations → GitHub Release from CHANGELOG → Zenodo) |
| Upkeep | Dependabot for actions, pre-commit hooks and uv.lock (monthly, grouped, 7-day cooldown; never raises minimum versions) |
| Project files | AGENTS.md, CONTRIBUTING.md, CHANGELOG.md, CITATION.cff, .zenodo.json, issue forms, PR template |
Generated projects score clean on the Scientific Python
repo-review
(deliberate exceptions are listed in [tool.repo-review]), and their workflows
pass actionlint and zizmor.
copier.yml questions, computed values (python_max, uv_version) and the after-copy message
template/ the project; files ending in .jinja are rendered, everything else is copied as is
justfile `just bake`, `just test` for working on the template
fixtures/ examples `just test` adds to the baked project, one of each kind the docs can run
.github/workflows/ template.yml bakes the template and runs every check in the result
See CONTRIBUTING.md to change the template.
MIT; see LICENSE.