Releasing PhilanthroPy
August 21, 2026 · View on GitHub
Maintainer runbook. Cutting a release needs PyPI, Zenodo, and GitHub release permissions; contributors do not need anything on this page. For contributing, see CONTRIBUTING.md.
Versioning & deprecation
PhilanthroPy follows Semantic Versioning. While the
project is pre-1.0, minor releases (0.x.0) may contain breaking changes; these
are always called out under a Breaking heading in
CHANGELOG.md. Where feasible, a deprecated public API is kept for
at least one minor release and emits a DeprecationWarning pointing at its
replacement before removal. Supported versions are listed in
SECURITY.md.
0.7.0 ships one deprecation, WealthScreeningImputerKNN(group_col_idx=...),
removed in 0.8.0, via an inline warnings.warn(..., DeprecationWarning) in
fit for a parameter that no longer does anything. tests/test_deprecations.py
is back for it, with a registry meta-test that fails when a shim ships
untested. philanthropy/utils/_deprecation.py, 0.6.0's
deprecated_alias(new_name, removed_in=...) decorator for a renamed method,
was removed at 0.7.0 alongside the shims it policed; reintroduce it the next
time a renamed method needs one. Per-symbol stability tiers live in
docs/reference/index.md.
RELEASE CHECKLIST
Run in order. Steps 1–6 are the gate publish.yml enforces; 7–9 are manual.
make ciis green, and so ispytest tests/test_public_api_contract.py -q. Also skim the## [Unreleased]block inCHANGELOG.md..gitattributessetsmerge=unionon that file so concurrent PRs stop conflicting on it, and the cost is that git will never again flag a problem in this file. Two things to look for: a bullet under the wrong heading (union is line-based, not section-aware), and a duplicated or contradictory bullet (if two branches edited the same entry, union silently keeps both). Cheap to eyeball once per release, annoying to find later.- Bump
versioninpyproject.toml. Nothing else carries the version:philanthropy.__version__reads it from installed metadata. - Add a
## [X.Y.Z] - YYYY-MM-DDsection toCHANGELOG.md. The date is required:publish.ymlrejects a heading still carrying- TBD, which is what a release staged ahead of its window looks like. A release that removes anything needs a Breaking heading; a release that adds a shim needs a Deprecated heading naming every alias and dead parameter with the version that removes it. - Update the "Deprecations" section of
docs/reference/index.md. python -m build && python -m twine check --strict dist/*.- Tag
vX.Y.Zand push it. Publishing is gated on clicking Publish release in the GitHub UI;publish.ymlthen re-checks that the tag,pyproject.tomlandCHANGELOG.mdagree before it builds.- Paste the CHANGELOG section into the release body. An empty release body wastes the only page most people ever read about a version.
- Add a
### Thanksline naming every external contributor in that section. A named credit on a permanent release page is worth more to a drive-by contributor than a line in a markdown file. - Update the supported-versions table in
SECURITY.mdso it names a version that can actually be installed. - Re-run
examples/quickstart.ipynbif the public API moved: it installs from PyPI (!pip install philanthropy -q) while living onmain, so it is the one artifact that can silently break on a rename.
- Confirm the release landed on PyPI and that
pip install philanthropy==X.Y.Zworks in a clean venv. - Deposit to Zenodo. The GitHub–Zenodo integration picks up the published
release and reads
.zenodo.json. JOSS requires a deposited archive with a DOI at acceptance. - On the first deposit only: copy the Zenodo concept DOI (the one that
resolves to the latest version, not the per-version DOI) into the
identifiers:stanza ofCITATION.cff, replacing10.5281/zenodo.PENDING, and commit.
A shim added in X.Y.0 may only be removed in X.(Y+1).0 after X.Y.0 is
published on PyPI: one full published minor of overlap, not one commit.
Cutting a release with several versions staged on main
main can carry more than one unreleased version; 0.7.0 and 1.0.0 are both
merged and both still - TBD. The gate compares the tag against the
pyproject.toml of the commit the tag points at, so what you can cut depends
on what that file reads.
pyproject.toml on main tracks the newest published release, currently
0.6.0, not the newest staged one. That is deliberate: a version nobody can
pip install breaks CITATION.cff, the Zenodo deposit and the JOSS archive
step, all of which have to name a release that exists.
So cutting the next release is the normal path: branch, bump, date, tag.
git switch -c release/0.7.0 main
# step 2: bump `version` in pyproject.toml to 0.7.0
# step 3: date the '## [0.7.0]' heading in CHANGELOG.md
git commit -am "chore: release 0.7.0"
git push origin release/0.7.0 && git tag v0.7.0 && git push origin v0.7.0
The trap to know about: if someone bumps pyproject.toml on the tip to a
later version than the one you are cutting, the older release can no longer be
tagged from the tip, because v0.7.0 against a tip reading version = "1.0.0"
fails with tag != pyproject 1.0.0, dated changelog or not. In that case tag the
last commit that still reads the version you want. Keeping main at the
published version, as above, avoids the situation entirely.
The branch is not a fork of the release pipeline. A release event only fires
for a workflow file that "exists on the default branch", so publish.yml is
always the one on main; actions/checkout with no ref "defaults to the
reference or SHA for that event", which is the tagged commit. The old tree gets
the current gate and the current action pins.
Then date the same heading on main, so its changelog stops claiming - TBD
for something that shipped.