Migrating from changesets
If you already run JS changesets, the model transfers directly. Molt keeps the changeset file, the two-phase workflow, and most config keys, and changes what Python's packaging rules give a different answer to.
The muscle memory carries over: you still add a changeset per change and version in a batch at release time. What differs are the places where PyPI and PEP 440 work differently from npm and SemVer, plus a few subsystems that have no Python meaning. This guide maps the concepts and lists the concrete differences. For a feature-by-feature table, see Molt and changesets.
What transfers unchanged
- Changeset files. molt reads the same
.changeset/*.mdformat -- YAML front matter mapping package names to bump types, then a Markdown summary. Your existing changesets carry over; the only substantive change is that names are matched using PEP 503 normalization, soAcme_Corematchesacme-core. - The workflow.
add->version->publishis the same loop, with the same "add intent while it is fresh, consume it in a batch" split. - Config keys, in camelCase. molt's config lives in
[tool.molt]inpyproject.toml, but it accepts your changesets keys via aliasing.baseBranch,updateInternalDependencies,fixed,linked,ignoreare all understood alongside their snake_case spellings (base_branch, and so on), so you can port most of a config table by pasting it. The keys molt does not carry have to be removed first -- see Your config needs editing before it loads. See also The config file. fixedandlinkedgroups,status,--since, dry runs. All present, with the same semantics.
What changed, and why
Versions and prereleases follow PEP 440
molt does version math with the packaging library -- the PyPA reference implementation of PEP 440 and PEP 508 -- not SemVer. Bump types are still major/minor/patch, but:
- A prerelease is
1.0.0rc0,1.0.0a1, or1.0.0.dev3from a fixed vocabulary, not an arbitrary1.0.0-next.0tag. - Range matching uses PEP 440 prerelease scoping, so a dependent that already opted into prereleases is not force-released as its dependency moves
rc0 -> rc1. This is a deliberate divergence from node-semver; see Versioning and PEP 440.
No pre.json -- prerelease is a flag
Entering prerelease mode in changesets writes a pre.json that becomes repository-wide branch state. Molt has no such file. Prerelease is an invocation flag:
There is no mode to enter, no file to commit, and no branch to unblock. A named channel such as next has no PEP 440 spelling, so map yours onto a, b, rc, or dev. See Prerelease mode and molt pre.
Snapshots target a non-PyPI index
On npm a snapshot is a throwaway version hidden behind a dist-tag. PyPI versions are permanent and it has no dist-tags, so every snapshot there would burn a public version number for good. Molt builds snapshots as PEP 440 .devN versions and targets a separate index by default: a wheelhouse, TestPyPI, or a private index. Publishing one to PyPI needs an explicit opt-in. See Snapshot releases.
molt yank replaces "unpublish"
PyPI has no unpublish. The recovery for a bad release is PEP 592 yanking: the release stays installable for anyone already pinned to it, but resolvers stop selecting it. Molt makes this a verb, molt yank, which checks the version and prints the exact steps. PyPI publishes no yank API, so the final click happens in your browser.
tag is now git-tag
changesets renamed tag to git-tag in v3 because "tag" collided with npm dist-tags. molt uses git-tag from the start. See molt git-tag.
peerDependencies are gone
Python has no peerDependencies, so molt drops the concept, along with the experimental onlyUpdatePeerDependentsWhenOutOfRange flag. Extras (foo[bar]) are the nearest analogue and behave like ordinary dependencies.
Your config needs editing before it loads
This is the one step of the migration that is not a paste, and it is worth doing first because nothing else runs until it is done. Molt refuses a configuration it cannot fully honour, naming the key or the value and what to write instead. It does not warn and carry on: a setting molt silently ignored would be a release molt got wrong, and by the time you read the warning the version is in a manifest and possibly on an index.
Run
molt doctorhere. It reports every rejected key and value in one pass, each naming the replacement, so you edit the file once instead of once per failed run. It also names every package your workspace has and tells you which ones a release would skip -- worth reading before your firstmolt version, because a package with no version, or a version derived from a git tag, is skipped and is invisible until a release is already going.
The keys a changesets configuration may carry that molt does not accept:
And one value, which is easy to miss because it is not a key at all:
That last row catches configurations that did nothing unusual: a stock changesets 3.0 setup carries format: "auto", so a straight paste fails. Every one of those backends is a Node program, and molt shells out to no Node, so accepting the value would mean promising formatting and doing nothing. Deleting the line is the right answer for almost everybody, because molt emits correct Markdown directly.
$schema is unaffected: molt accepts it, strips it, and says nothing. It drives editor autocomplete and is not a setting.
Molt also refuses combinations that cannot mean anything: changelog = { generator = false, template = "..." }, an unrecognised {placeholder} in snapshot.prerelease_template, an empty base_branch, and an empty changed_file_patterns list. None of these has a changesets counterpart to migrate; they are listed in the options reference.
What still only warns is a glob that matches nothing today: an ignore entry or a fixed / linked member naming a package that does not exist yet. That is a fact about your workspace at this moment, not a mistake in the file.
What molt does differently, at a glance
Seeding changesets from your history
Adopting molt mid-project leaves you with commits that predate any changesets. molt offers a one-time seeding step that proposes changeset files from your recent commit history, which you then edit and commit:
This is a migration aid. Molt does not derive versions from commit messages on an ongoing basis, because a commit is not a release intent and a prefix convention cannot see a breaking change that crosses a package boundary. Seeding gets you a starting pile of changesets; from there, the normal molt add flow takes over. See The changeset workflow.
A suggested migration order
- Move
.changeset/config.jsonsettings into[tool.molt](camelCase keys are accepted, so this is mostly a paste) and remove the keys and theformatvalue molt refuses -- see Your config needs editing before it loads. Run any molt command to check: it names every remaining problem in one pass. - Confirm the ecosystem backend discovers your workspace members.
- Keep any pending
.changeset/*.mdfiles; molt reads them as-is. - Replace
pre enter/pre exithabits withmolt version --pre. - Repoint snapshot publishing at a non-PyPI index.
- Optionally seed changesets for in-flight work with
molt add --from-commits.
Where to go next
- Molt and changesets -- the full feature table.
- Prerelease mode and
molt pre-- the flag that replacespre.json. - Snapshot releases -- why they target a separate index.
- Design decisions -- the rationale behind each divergence.