Pip is a tool that Python users know and love, but have you ever tried making your own project for the ecosystem? If so, then you know that your projects need to be versioned. Today, I’ll go over some versioning techniques and how you might actually go about incorporating them into your own projects.
Table of Contents
- Why Version Your Software?
- Versioning Methods
- Python Versioning
- Adding Version to Python Configuration Files
Why Version Your Software?
Software versioning is a technique that we use to mark points of interest along the history of a software project. If we think of software development as a linear process (it’s not, but humor me), then versions are those points along the way that we want to call attention to. There are many benefits of providing these signposts to our users, but here are a few off the top of my head:
- Users get to ignore the sea of changes to a software project and instead focus on a few key points called releases.
- Users gain access to a summarized history of a software project, often through diffs and changelogs
- Users are able to handpick their preferred versions and rollback versions when their software breaks
All-in-all, you’d be hard pressed to find folks in the community against versioning. After all, like documentation, versioning is all about effective communication with your users. Fortunately, there are a few ways to go about it.
Versioning Methods
Unfortunately, while versioning has benefits, there isn’t a single versioning method that everyone uses. There are a lot of different techniques floating around that you should at least be familiar with.
Semantic Versioning (SemVer)
When it comes to versioning software, the popular approach is semantic versioning. As the name implies, semantic versioning is a way of giving meaning to the version “number” attached to a particular release. Semantic versioning accomplishes this by providing three numbers separated by periods (e.g., 2.1.3). Each number in the version number sequence has a specific meaning:
- Major: represents the first number in the version number (e.g., 2.1.3); typically only incremented when changes are backwards incompatible.
- Minor: represents the second number in the version number (e.g., 2.1.3); typically incremented any time new features are added to the project.
- Patch (sometimes called micro): represents the third number in the version number (e.g., 2.1.3); typically incremented any time bugs are fixed.
It’s worth noting that there is significant debate around the appropriate way to apply semantic versioning. After all, there are no rigorous rules to follow when versioning software, so new releases are almost certainly going to cause problems for users regardless of the care we take as developers.
One perspective I found particularly interesting on this topic was from Joey Lynch who argued that major versions should be included directly in the package name. For Joey, it seems the issue lies with semantic versioning itself and that folks should be moving to a versioning system that causes less headache. In contrast, Hynek Schlawack argues that versioning is always going to be a headache, so it’s on the user of the dependency to be more careful around upgrading. Though, I don’t think Hynek cares for semantic versioning either.
As for me, semantic versioning is my preferred technique. There just isn’t a versioning technique, outside of Joey’s solution, that seems better to me. As a result, semantic versioning is something I use on all of my Python projects. That said, if you’re interested in what else is out there, keep reading.
Others
While semantic versioning is the technique you probably see most of the time, there are a few others. For example, some projects use a name-based versioning scheme—often mixed with semantic versioning. One project that comes to mind for me is macOs, which names many of its versions after large cats or locations.
Another versioning technique you might see is date-based versioning (with examples). The idea being that the version number should convey temporal meaning. Usually, these look like semantic versions (e.g., 23.04 for April 2023), but they don’t convey how the software has changed.
While the previous options for versioning are more sophisticated, most folks are probably familiar with the sequential versioning scheme for Word docs and other classroom work. This same technique can be used for software (e.g., v1, v34, etc.).
Python Versioning
While there are plenty of versioning techniques, Python somewhat reduces your options by providing a set of constraints on what a version can look like in PEP 440. I don’t personally recommend trying to read the document because it’s lengthy and full of jargon, but here’s the key piece (i.e., the versioning scheme):
[N!]N(.N)*[{a|b|rc}N][.postN][.devN]
In short, this scheme basically says that version numbers must be period separated strings of numbers, with the added ability to label a release with other attributes like alpha, beta, etc. Therefore, assuming you want to follow semantic versioning, you’re in luck; any semantic version you like will match this scheme. In fact, any of the following version strings are valid:
2.0.1 # standard semantic version 1.3.0a1 # semantic version in 1st alpha 5.1.0b3 # semantic version in 3rd beta 2023.04 # date-based version
Next, we’ll talk about how to actually include these version strings in a project.
Adding Version to Python Configuration Files
Before I get into how to actually version your project, I have to get something of my chest. In the original iteration of this article in April 2023, learned that pip no longer wants users making setup.py files. Because all of my projects were written using setup.py files, I shared how I include versions in them. For instance, here is the sample setup.py file I had in my SnakeMD repo:
import setuptools with open("README.md", "r") as fh: long_description = fh.read() MAJOR = 2 MINOR = 0 PATCH = 0 PRE = "" name = "SnakeMD" version = f"{MAJOR}.{MINOR}" release = f"{MAJOR}.{MINOR}.{PATCH}{PRE}" setuptools.setup( name=name, version=release, author="The Renegade Coder", author_email="jeremy.grifski@therenegadecoder.com", description="A markdown generation library for Python.", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/TheRenegadeCoder/SnakeMD", packages=setuptools.find_packages(), python_requires=">=3.8", install_requires=[], classifiers=[ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Operating System :: OS Independent", "Topic :: Documentation :: Sphinx", ] )
Here, you can see that I explicitly list out the different components of the version number (i.e., MAJOR, MINOR, and PATCH). In addition, I even include a PRE variable for when I want to mark a build as alpha or beta. Whatever values I place in those variables will be the values that show up when the project is deployed.
Now, there are some clear oddities in this setup file. One being that the version and release variables are separate. Worse, version is never used. I did a bit of digging and found that version used to be used for sphinx, which no longer supports setup.py integration (surprise, surprise). As a result, you could remove line 12 without consequence.
More recently, I replaced my setup.py file with a pyproject.toml file as follows:
# Poetry settings [tool.poetry] name = "SnakeMD" description = "A markdown generation library for Python." version = "2.2.0b1" license = "MIT" authors = [ "Jeremy Grifski <jeremy.grifski@therenegadecoder.com>" ] readme = "README.md" homepage = "https://www.snakemd.io" repository = "https://github.com/TheRenegadeCoder/SnakeMD" documentation = "https://www.snakemd.io/en/latest/docs/" classifiers=[ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Operating System :: OS Independent", "Topic :: Documentation :: Sphinx", ] [tool.poetry.urls] Changelog = "https://www.snakemd.io/en/latest/version-history/" [tool.poetry.dependencies] python = "^3.8" # Build system settings [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api"
With the new format, it’s a little bit hard to dynamically create the version number, but I like that everything is in one place. In the future, I might share how I actually deploy a project (you can always take a peek at my deployment workflow in the meantime). It’ll involve all the nitty gritty details like how to tag branches for releases and how to trigger deployments to PyPI. For now, I’m so thrown off by learning about the sunsetting of setup.py that I’m just going to call this article a wrap. Thanks for sticking around. If you liked this article and want to read more like it, here are some of my favorites:
- I Finally Figured Out Python’s Module and Package System
- The Complete Guide to SnakeMD: A Python Library for Generating Markdown
- Introduction to Python Coding With Discord Bots
And if you want to berate me for being a bad developer, come check out my list of ways to grow the site, which includes a link to my Discord. Otherwise, take care!
Recent Posts
One of the core features of modern programming languages is functions, but did you know Python has them too? Let's take a look!
Python has a cool feature that allows you to overload the operators. Let's talk about what that means and how you might use it!