molt
A release CLI for Python monorepos — changesets in, coordinated version bumps, changelogs and PyPI publishes out.
- python
- cli
- monorepo
- release
- pep-440
- uv
molt brings the changeset workflow to Python. You record the intent of a change while you write it; a separate step turns the accumulated intent into releases — version bumps, changelogs, dependency propagation, and a PyPI publish.
JavaScript has had changesets for years. Python has the same problem and no equivalent: a workspace of packages that depend on each other, where bumping one means finding every dependent, deciding how far each one has to move, and rewriting its pins by hand. That is the gap molt fills.
The release loop
molt init # one time: create .changeset/ and a [tool.molt] block
# ... make a change ...
molt add # record which packages changed, and how much
molt version # bump versions, write changelogs, update the lockfile
molt publish # build, upload to PyPI, tagmolt add writes a small Markdown file you commit next to your code:
---
'acme-core': minor
---
Add a --stream flag to the export API for large datasets.molt version consumes every pending changeset in one pass, takes the highest bump per package, folds the summaries into each CHANGELOG.md, and deletes the files it consumed.
Why it works this way
- Propagation is computed, not guessed. Given
acme-clirequiringacme-core>=1.2.0,<2.0.0, a major bump toacme-coreforces a patch release ofacme-cliand a rewritten pin. A minor bump to1.3.0releases nothing extra, because1.3.0still satisfies the range. The math is PEP 440 and PEP 508 throughpackaging, the PyPA reference implementation — not a homegrown parser. - Writes are buffered and flushed together. A failure mid-run never leaves a repository half-versioned.
- Prerelease is a flag, not a mode.
molt version --pre rc. There is no state file to enter, commit, or exit. uv.lockis refreshed on every bump, so a--frozeninstall in CI still resolves against the release commit. uv workspaces are first-class; other backends sit behind an ecosystem seam.- A bad release is yanked, not unpublished. PyPI versions are permanent, so
molt yankverifies the version and prints the exact steps (PEP 592). - stdout carries machine-readable payloads only.
molt status --output json | jq '.releases[].name'needs no filtering, and--dry-runprints the plan for any mutating command.
A companion GitHub Action, molt-action, runs the whole loop: it keeps a “Version Packages” pull request in sync with pending changesets, then builds, publishes and tags once that pull request merges.
Guides and the full CLI reference live at molt.gio-labs.com.