Release Policy

PyBaMM Release Policy#

This document is the source of truth for how PyBaMM is versioned, when releases happen, what counts as a breaking change, and how breaking changes and deprecations are communicated. For a quick summary aimed at users, see the Versioning section of the README.

At a glance#

Version scheme#

PyBaMM versions take the form YY.MM.N.P:

ComponentMeaningExample
YYTwo-digit year27
MMMonth of release (1–12, no leading zero)1, 11
NFeature release within that month, 0-indexed0 for the first0, 1
PPatch level — 0 for the feature release itself; 1, 2, … for patches off it0, 1, 2

Worked examples:

Cutover. The first feature release tagged after the policy lands is the first to use YY.MM.N.P. Earlier tags (v26.x.N) remain in their original form; we do not retroactively retag.

Monorepo: independent package releases#

PyBaMM and pybammsolvers live in one repository (a UV workspace under packages/) but release independently to PyPI, discriminated by tag namespace. See docs/superpowers/specs/2026-06-11-pybamm-monorepo-design.md for the full design.

Release cadence#

What counts as a breaking change#

A change is breaking if it does any of the following:

A change is not breaking if it:

“Public API” defined#

Public API = anything documented at docs.pybamm.org.

If a class, function, or method appears in the rendered API reference, it is in PyBaMM’s public contract. Anything else — internal modules, leading-underscore names, undocumented helpers — is private and may change between any two releases without a ## Breaking changes entry.

The rendered docs build is the auditable source of truth. A PR reviewer can answer “is this public?” by checking whether the symbol is reachable from the API reference index.

Deprecation policy#

Removing or renaming a public API must ship a DeprecationWarning in at least two prior feature releases before the removal release.

Exceptions. The two-release floor may be skipped only when one of the following applies. Each exception must be justified in the PR description and in the ## Breaking changes changelog entry:

Pre-announcement#

The # [Unreleased] section at the top of CHANGELOG.md on main is the canonical pre-announcement channel. Downstream package maintainers, users, and contributors can read that section at any time to see what is about to ship.

The following are not required by policy but may be chosen by maintainers for exceptional breaks (e.g. removing a long-standing core class, dropping a supported Python version):

Changelog conventions#

Within each release block (and within # [Unreleased]), sections appear in this order, omitting any that are empty:

## Breaking changes
## Deprecated
## Features
## Bug fixes

The ordering is scariest first: anyone scanning the changelog sees breaks and deprecations before features.

Entry format. Each entry is a single bullet ending in a PR link:

- Short imperative description of the change. ([#1234](https://github.com/pybamm-team/PyBaMM/pull/1234))

Use full GitHub URLs (not bare #1234) so the rendered markdown on docs.pybamm.org links correctly. ## Breaking changes and ## Deprecated entries must include a one-line migration note describing how users adapt.

Historical entries are not rewritten. The new section ordering and the new ## Deprecated section apply from the next release block forward. Existing release blocks keep whatever they currently have.

Release-manager checklist#

The version string in the built distribution is supplied by hatch-vcs from the VCS tag (pyproject.toml has dynamic = ["version"] with version.source = "vcs", writing to src/pybamm/_version.py). The scripts/update_version.py helper updates CITATION.cff and prepends a new dated heading to CHANGELOG.md; its version-string handling is format-agnostic and accepts the four-component form unchanged.

Cutting a feature release#

A feature release is YY.MM.N.0 — the patch component is 0. The first feature release in a given calendar month uses N=0; subsequent feature releases in the same month use N=1, N=2, etc.

  1. Confirm # [Unreleased] in CHANGELOG.md accurately reflects what’s about to ship — every breaking change, deprecation, feature, and bug fix has an entry with a PR link, and entries are grouped under the four sections (## Breaking changes, ## Deprecated, ## Features, ## Bug fixes) in that order.
  2. Create and check out a release branch from main: git checkout -b release/vYY.MM.N.0.
  3. Run uv run python scripts/update_version.py YY.MM.N.0 to update CITATION.cff and prepend a dated heading to CHANGELOG.md.
  4. Push the branch and open a PR to main. Ensure CI passes, then merge.
  5. From main at the merge commit, create a GitHub release with the tag pybamm-vYY.MM.N.0. Copy the relevant CHANGELOG.md block into the release description. This triggers publish_pypi.yml and creates the PyPI release automatically. (Monorepo: PyBaMM release tags use the pybamm-v prefix so they are distinct from pybammsolvers-v*; see “Monorepo: independent package releases” above.)
  6. Verify the release installs cleanly: pip install pybamm==YY.MM.N.0.

Cutting a patch release#

A patch release is YY.MM.N.P where P >= 1. Patches are cut from the previous tag in the same feature line so the release contains only the bug fixes, not unrelated changes that have landed on main since the feature release.

  1. Ensure all bug fixes are merged to main first via normal PRs.
  2. Create a new branch from the previous tag in the same feature line: git checkout -b release/vYY.MM.N.P pybamm-vYY.MM.N.{P-1} (e.g. release/v27.1.0.1 from pybamm-v27.1.0.0).
  3. Cherry-pick the bug fixes onto the new branch, recording the original SHA with -x:
    git cherry-pick -x <commit-sha-from-main>
  4. Run uv run python scripts/update_version.py YY.MM.N.P to update CITATION.cff and prepend a dated heading to CHANGELOG.md. Commit the result on the release branch.
  5. Create a GitHub release with the tag pybamm-vYY.MM.N.P from the release/vYY.MM.N.P branch (NOT from main). Copy the relevant CHANGELOG.md block into the release description. This triggers publish_pypi.yml.
  6. Verify the release installs cleanly: pip install pybamm==YY.MM.N.P.
  7. Update the changelog on main separately. Do not merge the release branch back to main — that would duplicate commits with new hashes. Instead:
    git checkout main
    git checkout -b update-changelog-vYY.MM.N.P
    Edit CHANGELOG.md to add the new dated vYY.MM.N.P block (moving the entries out of # [Unreleased]), and update CITATION.cff. Open a PR to main.
  8. Delete the release branch after tagging — it is no longer needed.

Cutting a pybammsolvers release#

pybammsolvers releases independently of PyBaMM. Its published version is read from packages/pybammsolvers/src/pybammsolvers/version.py (via the regex in packages/pybammsolvers/pyproject.toml), not from the release tag — the pybammsolvers-v* tag namespace only routes the workflow. Keep the tag and version.py in lockstep, or the wrong version ships.

  1. Bump __version__ in packages/pybammsolvers/src/pybammsolvers/version.py and record the change in CHANGELOG.md. Open a PR to main, ensure CI passes, then merge.
  2. From main at the merge commit, create a GitHub release with the tag pybammsolvers-vX.Y.Z, where X.Y.Z exactly matches the new version.py value. This triggers release_solvers.yml, which builds wheels + sdist and publishes to PyPI. The check_version job in that workflow fails the release if the tag and version.py disagree; PyPI separately rejects a re-upload of an already-published version.
  3. Verify the release installs cleanly: pip install pybammsolvers==X.Y.Z.

Conda-forge#

The conda-forge release flow is triggered automatically after a stable PyPI release: the conda-forge bot opens a PR against pybamm-feedstock, which maintainers review and approve.

When a release touches the API, console scripts, entry points, optional dependencies, supported Python versions, or core project metadata, update meta.yaml in pybamm-feedstock following the conda-forge maintainer docs and re-render the recipe. Push updates directly to the bot’s automated PR where possible. Manual PRs must bump the build number in meta.yaml and be opened from a personal fork.

On this page