Click Use this template → Create a new repository (this is a GitHub template repository), then:
1. Rename the placeholders (two tokens — find & replace across the repo)
project-template→ your PyPI/distribution name (hyphens): inpyproject.toml([project].name),.github/workflows/release.yml(pypi-package-name), andmkdocs.yml.project_template→ your import/package name (underscores): theproject_template/directory, plus its--cov=/packages/pythonpathreferences inpyproject.toml.- If not publishing docs, delete
mkdocs.yml,docs/, and.github/workflows/docs.yml; otherwise setdocs-urlinrelease.ymland uncomment themkdocs-*dev deps.
2. Install + pre-commit hook
pip install poetry poethepoet && poe install3. Create the develop branch
git checkout -b develop && git push origin develop4. GitHub repo settings
- General → enable Allow auto-merge (required for Dependabot auto-merge).
- Branches → your stable branch (
mainormaster): read-only, require thetest / checkstatus, restrict push togithub-actions[bot];develop: requiretest / check. (test / checkis the matrix gate — one stable check regardless of how many Python versions run.) - Environments → create
release: deployment branchdeveloponly, required reviewer.
5. PyPI trusted publishing (once per package) — pypi.org → your
project → Publishing → Add trusted publisher: Owner = the GitHub user or org that owns your
repo, Repository = your repo, Workflow release.yml, Environment release. No
PYPI_API_TOKEN needed.
The reusable CI/CD lives in ParkerIndustries/workflows;
the .github/workflows/*.yml here are just the trigger stubs that call into it. See that
repo's README for the release flow, the Dependabot permissions requirement, and the
private-repo PAT setup.
Short description of your package.
- Python 3.12+
- Poetry 2.0+
poetry install && poetry run poe installThis installs all dependencies and sets up the pre-commit hook that runs poe check before every commit.
Two long-lived branches:
develop— the integration branch. All work lands here.main(ormaster) — the stable/released branch. Protected and read-only; only the release automation pushes to it.
Day to day:
- Branch off
developfor your change (feature/…,fix/…). - Open a PR into
develop. CI runs the QA matrix; thetest / checkgate must pass to merge. - Dependabot also opens dependency PRs into
develop, auto-merged once their checks pass.
Releasing (maintainers) — Actions → Release → Run workflow, on develop:
QA matrix + pip-audit ← release stops here on failure; nothing is pushed
bump patch version (develop)
build + publish to PyPI ← trusted publishing; stops here on failure, stable untouched
fast-forward develop → main/master
tag vX.Y.Z + signed GitHub Release (auto changelog)
Releases are always cut from develop; the stable branch only ever moves forward via this
fast-forward, so it stays a clean linear history of released versions. The changelog is the
commit subjects since the last tag (plus changed docs pages if docs-url is set).
Tasks are managed with poethepoet and run via poetry run poe <task>.
| Task | Description |
|---|---|
poe install |
Install deps and register the pre-commit hook |
poe check |
Run the full quality pipeline (lint, format, type-check, test, build) |
poe check --ci |
Same as above, plus git diff --exit-code to catch uncommitted changes |
poe docs |
Serve docs locally with live-reload (only if you kept mkdocs.yml + docs/) |
The poe check pipeline runs the following steps in order, collecting all failures before exiting:
- ruff (lint + format)
- ty (type check)
- pytest
- poetry's
checkandbuild
The same pipeline is included in CI check and pre-commit hook, but still recommended to run manually after changes.
Customizing the checks. The whole QA stack is just the [tool.poe.tasks.check] shell
script in pyproject.toml — this repo owns it, not the shared CI. To swap the type checker
(the template defaults to ty) for mypy or pyright, add
the tool to the dev deps and change the one line, e.g. poetry run mypy . || code=1. Add
or remove steps the same way. CI runs exactly this task (poe check --ci), so it stays in
sync automatically.
Dependency vulnerability scan (pip-audit) runs on release only, not on everyday
PRs/pushes — a newly-disclosed CVE in a transitive dep shouldn't red unrelated PRs, but it
should block a release. Run it locally anytime with poetry run pip-audit. When a CVE has
no fix yet, add --ignore-vuln <ID> where it runs.
CI runs the QA pipeline across a matrix of Python versions (default ["3.12", "3.13"],
defined centrally in the shared test.yml). Override per-repo in .github/workflows/test.yml
with python-versions: '[...]'. Branch protection should require the single test / check
status — a gate over the whole matrix, so it doesn't change when the matrix does.
The stubs trigger on pushes to main/master/develop and on all PRs; if your stable
branch is master, the release workflow needs stable-branch: master (see the release stub).
Pure-Python by default. If you need Cython extensions, see the commented setup.py sketch
and dev deps in pyproject.toml, and set matrix-build: true in the release stub — compiled
wheels are Python-version-specific, so the release then builds a wheel per version (plus one
sdist) instead of the single pure-Python py3-none-any wheel.
Tests are run with pytest with async support via pytest-asyncio (or pytest-trio), auto mode enabled. Shared fixtures go in tests/conftest.py.
poetry run pytest # run all testsBut pre-installed plugins may show a deeper insights (can be combined):
pytest --cov=your_moduleShows what percentage of the code is tested and what code paths weren't called. --cov-report option saves the result to a file so that it can be viewed with other tools, tracked, compared, uploaded, etc.
pytest --timeout=2Limits test execution time and avoid long (sometimes minutes-long and even hours-long) timeouts.
Alternative usage per test:
import pytest
@pytest.mark.timeout(2) # seconds
def test_timeout():
do_something()Run each test multiple times. Useful for triggering not-consistent bugs, dealing with probabilities, and leaks.
pytest --count=10Alternative usage per test:
import pytest
@pytest.mark.repeat(10)
def test_repeat():
do_something()Measures and compares execution time of a specific function, providing stats like min, max, mean, and median execution time, and allows saving/loading results for performance tracking. Pro tip: combine with pytest-profiling.
@pytest.mark.benchmark
def test_my_function(benchmark):
def func():
sum(range(1000))
benchmark(func) # wrap the func you want to benchmark- wrap the func you want to measure in
benchmark - if pytest.mark.benchmark is added, the benchmark test is skipped during running normal tests
- use
pytest --benchmark-onlyto run benchmarks
Tracks program’s function calls, execution time, and memory usage, helping find bottlenecks and optimise performance. ELI5: shows which function makes the code slow.
pytest --profile generates a profiling report which can be visualized with tools like snakeviz (more features). Alternatively, a simpler built-in option can be used - pytest --profile-svg generates an svg file that can be viewed in a web browser.
A basic alternative to bencmarking and profiling. Just log system resource usage and execution duration time.
pytest --resource-monitor --durations=0--durations=N shows the slowest N tests. 0 to show all tests’ durations
See LICENSE. This template ships Apache-2.0 as a placeholder — replace it with your project's own license (the template's own license is Apache-2.0 regardless).