CLI tool
python-semver/python-semver avatar
python-semver/python-semver

python-semver: semantic version handling for Python

Python package to work with Semantic Versioning (https://semver.org/)

526 stars117 forksPythonBSD-3-Clause

At a glance

What is it?
Semantic versioning is a small specification with a surprising number of edge cases, and this module is the Python answer to it: a parser that turns a version string into an object, a comparison function, and a set of functions that raise a version to the next one. What is interesting about the design is that it is entirely functional, so a version object is a value you pass around rather than a thing you mutate.
Who is it for?
The semver module fits a project whose versions come from somewhere other than a human, a build pipeline that has to decide whether a tag is newer than a released one, and a release tool that needs to compute the next version without guessing. It does not fit somebody who needs the full ecosystem of version ranges and constraints, since the module is the core format rather than a constraint solver.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the module is for

The module is a Python helper for working with semantic versioning, and the quickstart explains the scheme itself in three bullets before showing any code. A major version when you make incompatible interface changes, a minor version when you add functionality in a backwards compatible manner, and a patch version when you make backwards compatible bug fixes. Additional labels for pre-release and build metadata are supported.

So the library's job is narrow and its rules are fixed. The format is a specification published elsewhere, and the module's value is in applying it consistently: parsing a version string into its parts, comparing two versions, and producing the next version from an existing one.

And the interesting part is what it does not do. There is no range syntax, no constraint expressions, no wildcard matching, and no dependency resolution. This is the format, not a package manager's view of it.

That narrowness is a feature for the intended use, which is a script or a build step that has a version string in front of it and needs to know what it means. A pipeline that has to decide whether a tag is newer than what it shipped, a release script that has to work out the next version, a compatibility check between two components: all of these want the format and none of them want a constraint solver.

The module is distributed on the package index and documented on a hosted documentation site, and the readme is written in the markup dialect that the documentation system uses, which is the same dialect as the change log and the contribution guide, so the whole project speaks one language. The badge row covers the usual ground and a couple of unusual ones: a percentage of open issues, which is a maintenance signal rather than a feature signal, and a badge for the formatter the project uses, which is a convention in the Python ecosystem and a small declaration of style.

Parsing, and the object you get back

Parsing is a single call, and the quickstart shows the example with a version that has every part filled in: a pre-release label and a build label after the patch number.

The result is an object with one attribute per part of the format. The major, minor and patch attributes are integers. The pre-release attribute is the pre-release string, and the build attribute is the build metadata string. So a version that carries both labels keeps both, and the caller can read either of them without parsing the string again.

That separation is worth pausing on, because it is the distinction the specification makes and the one implementations most often get wrong. The pre-release label is part of the version's identity in the ordering sense, and the build label is not: two versions that differ only in build metadata are the same version for the purposes of precedence. A module that stores them in one string has to re-parse it on every comparison; a module that stores them in separate fields compares the fields it needs and ignores the rest.

The object also has a readable representation, and the readme shows it. The parts are printed by name with their values, and a version with no pre-release or build label prints those as absent rather than as empty. That is a small thing that makes a difference when you are reading a log or a test failure.

And the object is the only thing the module returns. There is no dictionary, no tuple, and no optional result. A version string that does not follow the format raises, which is the right behaviour for a library whose purpose is to make sure the format is respected: silently returning a partial parse is how a version comparison ends up quietly wrong.

So the parse step is both the entry point and the guarantee. Everything else in the module operates on the object this produces.

Bumping, and the rule that makes it composable

The bump functions are the part of the design I would point at, and the reason is the first sentence of that part of the readme: the function leaves the original object untouched and returns a new instance with the raised part.

So a version object is a value. Calling the function on it does not change it; it produces another one. That is a small decision that has a large effect on how the functions compose.

And the readme says immediately that it is allowed to concatenate different bump functions, and shows it: raising the major and then raising the minor of the result gives a version whose major is the new major and whose minor is one, with the patch reset. So the composition is not an accident of the implementation, it is a documented and supported behaviour, which means you can write the next version as a chain and the semantics of the chain are pinned down.

The reset behaviour is in there too, and it is the semantically correct one. Raising the major gives a version whose minor and patch are zero, because a new major version starts again from the beginning. That is not obvious from the name of the function, and it is the sort of thing a library should state rather than leave to be inferred from a test.

The functional style also means the module does not need a copy constructor, a setter, or a notion of a mutable current version. There is no way to have two parts of a program disagreeing about what the current version is, because there is no current version; there are versions, and you pass them around. For a library whose output is often compared against a version read from a file, a version read from a tag, and a version computed from the previous one, that is a genuinely useful property.

The cost is that the chaining is a little more verbose than an in-place mutation would be, and that a caller who wants the new version has to keep the result. In exchange, the call sites cannot accidentally change a version that another part of the program is holding.

Comparing, and why the return value is a sign

Comparison is one function, and the readme gives three calls and their results, which is the clearest possible specification of the behaviour: one version below another gives minus one, the other way round gives one, and two equal versions give zero.

The choice to return a sign rather than a boolean is the interesting one, and it is the difference between a comparison that answers a question and a comparison that is usable in a sort.

A boolean function, is this version newer than that one, forces every caller that needs to order a list of versions to do the comparison itself, which means the caller writes the ordering logic, and two callers will write it differently, and one of them will get the equality case wrong. A sign is the primitive that a sort wants, and the module is then usable in a sorted collection without any adapter.

It is also the primitive that other languages already use, so a Python developer reading the function signature knows what to expect, and the zero case is unambiguous in a way that a boolean is not, since a boolean has no way to say that two things are equal other than by using a different operation.

The other thing worth noting is what comparison does not do. There is no exception for versions that cannot be compared, and there is no error for comparing a version with a pre-release label against one without. The ordering rules for pre-release labels are part of the specification, and the module applies them, so the caller does not have to know them.

The readme's line about other functions to discover is the sign of a small, complete library rather than a sprawling one. Four operations: parse, compare, bump, and the version object. That is the whole format.

The repository, and the project hygiene

The top level of the repository is a Python project's layout with several entries that tell you how the project is run, and the two most interesting are the ones about changes.

There is a change log file and, next to it, a directory for change fragments. That pairing is the convention where each pull request adds a small file describing its own change, and the fragments are collected into the change log at release time. It means the change log is written by the people who made the changes rather than reconstructed afterwards, and it means a pull request cannot forget to mention itself.

There is also a file describing the release procedure, which is the document that says what a release actually involves. For a library with a version that is computed rather than written, that matters more than usual, and the file is at the root where a maintainer will look.

And there is a citation file, which is a small thing that says the project considers itself citable, and which is the sort of courtesy that costs nothing and is very welcome to somebody writing a paper that used the library.

The rest of the top level is the conventional set: a contribution guide in the same markup dialect, a contributors file, a support document, a licence file, a manifest for the source distribution, a configuration file for the test runner, a coverage configuration, an editor configuration, a linter configuration, a documentation directory with a hosted documentation configuration, a task file, a configuration file for running the tests in several environments, a lock file for the fast package manager, and the source and test directories.

Two details in the project manifest are worth a paragraph each. The first is the version, which is declared dynamic, and next to it a commented-out requirement for the tool that computes it. The requirement being commented out is a transition in progress: the version is still dynamic, but the mechanism that produced it is being moved from an explicit dependency to something implicit, and the comment is left in place so that whoever completes the move knows what was there.

The second is the licence block, where the file reference form is used and the older inline form is commented out beneath it, with a reference to the packaging standard that introduced it. So the manifest is in the middle of two migrations at once, and in both cases the author has left the old form in the file as a note rather than deleting it.

The authorship is arranged in two lists, two authors and three maintainers, which is a small signal about the shape of the project: it was started by one person and continued by a slightly larger group, which is a healthy arrangement for a library with this many users.

Editorial conclusion

The semver module fits a project whose versions come from somewhere other than a human, a build pipeline that has to decide whether a tag is newer than a released one, and a release tool that needs to compute the next version without guessing. It does not fit somebody who needs the full ecosystem of version ranges and constraints, since the module is the core format rather than a constraint solver. Verify first whether your version strings are strictly in the specified format, because a parser that follows the specification will reject the informal versions people type, and that the pre-release and build fields survive the operations you perform, since the two are carried separately and only one of them is part of the ordering.

Frequently asked questions

What does the semver Python module do?

It implements the semantic versioning format: parsing a version string into an object with the major, minor and patch parts and the pre-release and build labels, comparing two versions, and raising a version to the next one. It does not implement version ranges or constraints, so it is the format itself rather than a dependency resolver's view of it.

Do the bump functions in semver modify the version in place?

No. Each bump function leaves the original object untouched and returns a new version instance with the raised part, and the functions can be chained, so raising the major and then the minor gives a version whose minor is one and whose patch is reset. The functional style is what makes the chaining well defined.

How does the semver compare function signal the result?

It returns a sign: minus one when the first version is below the second, one when it is above, and zero when they are equal. Returning a sign rather than a boolean makes the function usable directly in a sort and leaves the equality case unambiguous, and the ordering rules for pre-release labels are handled by the module rather than by the caller.

What happens when the semver module parses a version string that does not follow the format?

Parsing raises rather than returning a partial result, which is the right behaviour for a library whose purpose is to make sure the format is respected. A string that does not follow the specification is a bug in the producer, and the module refuses to paper over it.

How are changes recorded in the python-semver repository?

With a change log file and, beside it, a directory of change fragments. Each contribution adds a small file describing its own change and the fragments are collected into the change log at release time, so the log is written by the people who made the changes. There is also a separate document describing the release procedure and a citation file for the project.

Official sources

  1. License: BSD-3-Clause
  2. Project website
  3. python-semver/python-semver on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/python-semver-python-semver.svg)](https://hysenlabs.com/projects/python-semver-python-semver)

Was this useful?