How to Version Your Python Projects for Pip

How to Version Your Python Projects for Pip Featured Image

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?

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:

  1. Users get to ignore the sea of changes to a software project and instead focus on a few key points called releases.
  2. Users gain access to a summarized history of a software project, often through diffs and changelogs
  3. 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 nameOpens in a new tab.. 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 headacheOpens in a new tab., 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 examplesOpens in a new tab.). 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 440Opens in a new tab.. 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 repoOpens in a new tab.:

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 integrationOpens in a new tab. (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 meantimeOpens in a new tab.). 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:

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!

Jeremy Grifski

Jeremy grew up in a small town where he enjoyed playing soccer and video games, practicing taekwondo, and trading Pokémon cards. Once out of the nest, he pursued a Bachelors in Computer Engineering with a minor in Game Design. After college, he spent about two years writing software for a major engineering company. Then, he earned a master's in Computer Science and Engineering. Today, he pursues a PhD in Engineering Education in order to ultimately land a teaching gig. In his spare time, Jeremy enjoys spending time with his wife and kid, playing Overwatch 2, Lethal Company, and Baldur's Gate 3, reading manga, watching Penguins hockey, and traveling the world.

Recent Posts