Automatic semver bumps for Python packages, driven by AST-level public API change detection
Project description
semverer
Automatic semantic versioning for Python packages.
semverer inspects your package's AST, compares its public API against a
stored baseline, and bumps the version in pyproject.toml accordingly — so
you get correct major/minor/patch bumps for free, without having to think
about it. Run it standalone, in CI, or as a pre-commit hook.
How it works
semverer initsnapshots your package's public API (function/class/method signatures plus per-module implementation hashes) into[tool.semverer.baseline]in yourpyproject.toml.- On every subsequent
semverer check/semverer update, the current AST is compared against that baseline and each difference is classified. - The highest-severity difference decides the bump;
updatewrites the new version and refreshes the baseline. Writes go through tomlkit, so your file's formatting and comments are preserved.
The baseline is stored as flat, human-readable signature strings — diffs of
your pyproject.toml show exactly which API surface changed:
[tool.semverer.baseline.api]
"mypkg/cli.py::update" = "def update(package_path, *, dry_run=...)"
"mypkg/core.py::Engine" = "class Engine(Base)"
"mypkg/core.py::Engine.start" = "async def start(self, timeout=...)"
(Default values are normalized to ...; changing a default's value is a
patch-level implementation change, not an API change.)
Severity rules
| Change | Bump |
|---|---|
| Public symbol or module removed | major |
| Required parameter added; parameter removed, renamed, or reordered | major |
| Default removed (parameter becomes required) | major |
*args/**kwargs removed; def ↔ async def; parameter kind changed |
major |
| Base class removed or reordered | major |
| New public symbol or module | minor |
New parameter with a default; new *args/**kwargs |
minor |
| Parameter gains a default; base class added | minor |
| Implementation changed, public API identical (incl. private code) | patch |
| Comments/formatting only | none |
Public API = top-level functions and classes (and their methods) whose
names don't start with _, in modules whose names don't start with _ —
plus __init__.py and dunder names, which are public. Nested functions are
implementation detail. Type annotations are ignored in v1 (they don't change
runtime compatibility).
The full executable specification lives in features/ as
Gherkin scenarios, bound to tests with pytest-bdd.
Note: implementation hashes come from a canonical structural serialization of the AST, designed to be stable across Python minor versions (rendered text like
ast.unparseis not — f-string quoting changed in 3.12, for example). The baseline records which interpreter wrote it (python = "3.14"); if a future Python ever changes AST shape for existing syntax, patch findings made under a mismatched interpreter are annotated with a note suggestingsemverer initunder the project's pinned Python.
Install
pip install semverer # or: uv tool install semverer
Usage
semverer init # one-time: establish the baseline for the current version
semverer check # report the required bump (exit 1 if one is needed) — CI gate
semverer update # apply the bump and refresh the baseline — pre-commit hook
The package directory is auto-detected from [project].name (src/<name>/
or <name>/ layout). Override it in config or per-invocation:
[tool.semverer]
package = "src/mypkg"
semverer check src/mypkg --pyproject path/to/pyproject.toml
Exit codes follow the pre-commit convention: 0 nothing to do, 1 action
needed / files modified, 2 configuration error.
If you bump the version by hand, semverer respects it: a manual bump at least as large as the required severity is accepted instead of bumped again.
Auditing existing history
semverer audit # every commit on the current branch
semverer audit --tags-only # only published v* release tags
semverer audit --since v1.4.0 # from an adoption point forward
audit replays your git history: for each pair of consecutive commits (or
tags), it extracts both API snapshots directly from the git blobs and checks
that the recorded version moved at least as far as the rules require. Under-
bumps are violations (exit 1); over-bumps are allowed and noted. Run it
before init on an existing project to see how honest your versions have
been — and run it in CI like this repo does (semverer audit --tags-only),
where semverer's own published tags are its permanent integration test.
v0.1.0..v0.2.0: required minor, version 0.1.0 -> 0.2.0 OK
semverer: audit passed (1 OK, 0 skipped)
As a pre-commit hook
repos:
- repo: https://github.com/bubthegreat/semverer
rev: v0.1.0
hooks:
- id: semverer
When your public API changes, the hook updates pyproject.toml and fails the
commit; git add pyproject.toml and commit again. Use the semverer-check
hook id instead if you only want enforcement without writes.
Ordering tip: list semverer after your lint/type/test hooks and set
fail_fast: true at the top of your .pre-commit-config.yaml — pre-commit
runs all hooks even after a failure by default, so without fail_fast a
version bump could land alongside failing tests. This repo's own
.pre-commit-config.yaml shows the pattern
(ruff → mypy → pytest → semverer).
As a Claude Code skill
semverer skill install # into this project's .claude/skills/
semverer skill install --user # into ~/.claude/skills/ for all projects
Claude Code then knows to run semverer check/update whenever it changes
Python code in a semverer-managed package.
Known limitations (v1)
- Literal
[project] versionrequired. Dynamic versioning (dynamic = ["version"], hatch-vcs, setuptools-scm) and Poetry's[tool.poetry]metadata are not supported; semverer needs a version field it can read and rewrite. Versions must be valid semver (MAJOR.MINOR.PATCH— calver like2026.1is rejected with a clear error). - One package per pyproject. Monorepos with several published packages
under one
pyproject.tomlare out of scope. - Only statically visible definitions are API. Symbols defined inside
if/tryblocks (e.g.TYPE_CHECKINGstubs, import fallbacks), lambdas assigned to names, and anything built dynamically are invisible to signature comparison — changes to them surface as patch-level via the implementation hash. - Same-name redefinitions collapse.
@typing.overloadstacks and@property/setter pairs share one name; the last definition's signature wins. Changes to the others are patch-level. __all__is not consulted. Public/private is determined by naming convention only (leading underscore).- Decorators are not interpreted. A decorator that rewrites a function's real signature (e.g. some wrappers) is not seen through.
Development
uv sync --all-groups
uv run pre-commit install # one-time: makes git commit run the hook chain
uv run pytest
The Gherkin features in features/ are the spec; new behavior starts with a
scenario there. Unit tests in tests/unit/ cover extraction and
classification edge cases.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file semverer-0.2.0.tar.gz.
File metadata
- Download URL: semverer-0.2.0.tar.gz
- Upload date:
- Size: 69.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
edf843c3c5267f6378a282815fd1cd0b9cb27203bda2a4d1a6479242b467f89a
|
|
| MD5 |
6081473f907856ff2a80712c854ed3eb
|
|
| BLAKE2b-256 |
b0bcdea7c1919f2f286ee1b7cc0b222b2d19861cf822f450d2aa5ba8c800e1af
|
Provenance
The following attestation bundles were made for semverer-0.2.0.tar.gz:
Publisher:
publish.yml on bubthegreat/semverer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
semverer-0.2.0.tar.gz -
Subject digest:
edf843c3c5267f6378a282815fd1cd0b9cb27203bda2a4d1a6479242b467f89a - Sigstore transparency entry: 1789758842
- Sigstore integration time:
-
Permalink:
bubthegreat/semverer@4bc4e484b2867eaf090e2224f8faea24178500be -
Branch / Tag:
refs/heads/main - Owner: https://github.com/bubthegreat
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4bc4e484b2867eaf090e2224f8faea24178500be -
Trigger Event:
push
-
Statement type:
File details
Details for the file semverer-0.2.0-py3-none-any.whl.
File metadata
- Download URL: semverer-0.2.0-py3-none-any.whl
- Upload date:
- Size: 21.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b197b29f21d9641234eda943d88205c0e24cd80f7cb63e9cfd5399318f82f93d
|
|
| MD5 |
58f77da3ca71f0f58f4cfaf74b722f26
|
|
| BLAKE2b-256 |
bd5910f47fba43e04bac989d51041bf342aa1e4ab3fc99c0005685f1d015e7db
|
Provenance
The following attestation bundles were made for semverer-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on bubthegreat/semverer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
semverer-0.2.0-py3-none-any.whl -
Subject digest:
b197b29f21d9641234eda943d88205c0e24cd80f7cb63e9cfd5399318f82f93d - Sigstore transparency entry: 1789758920
- Sigstore integration time:
-
Permalink:
bubthegreat/semverer@4bc4e484b2867eaf090e2224f8faea24178500be -
Branch / Tag:
refs/heads/main - Owner: https://github.com/bubthegreat
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4bc4e484b2867eaf090e2224f8faea24178500be -
Trigger Event:
push
-
Statement type: