Skip to content
CLI tools

molt

A release CLI for Python monorepos — changesets in, coordinated version bumps, changelogs and PyPI publishes out.

Created Last commit
  • 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, tag

molt 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-cli requiring acme-core>=1.2.0,<2.0.0, a major bump to acme-core forces a patch release of acme-cli and a rewritten pin. A minor bump to 1.3.0 releases nothing extra, because 1.3.0 still satisfies the range. The math is PEP 440 and PEP 508 through packaging, 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.lock is refreshed on every bump, so a --frozen install 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 yank verifies 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-run prints 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.