5 Best Practices for Versioning Your Python Packages


Crista Perlton

Crista Perlton


Why You Should Create a Package Approval Workflow for  27th September, 2023

Do I really need to leave .NET Framework for .NET 8? 13th September, 2023


5 Best Practices for Versioning Your Python Packages

Posted on .

Versioning Python packages is tricky due to its range of complex versioning schemes, but that doesn’t mean you can just choose any old versioning scheme and call it a day. Especially when you’re developing packages for other teams in your organization.

Just as the package name must clearly communicate the content of the package, version numbers should also be used with purpose. Fortunately, learning how to version Python packages doesn’t mean you’ll need to parse PEP 440 – I’ve already done that for you!

This article will explain 5 best practices to speed up the development process, keep things organized, and avoid future headaches

Why are Version Numbers so Important? 

Version numbers are used to identify one package from another and communicate changes made. Without version numbers, identifying what changes were made in which version would be a logistical nightmare. Whoever is responsible for communicating changes would get confused and eventually the wrong package would get deployed. 

Another important function of version numbers is managing package dependencies. Packages almost always come with dependencies. Dependencies will often have their own dependencies, leading to a “dependency tree” of required packages. 

Dependency resolution can get complex fast. If you’re building an application with Python and two packages require different versions of the same package, then Python will have a version conflict and your project may not build! 

How to Select a Version Number 

Until 2009, there were no standards for how distribution packages in Python should be versioned. Packages were versioned with unclear and inconsistent schemes like 1.5.2b2, 3.4j, 1.13++, etc. These made it virtually impossible to know what the latest version of a package was or whether it was prerelease or stable. 

Python’s version identification and dependency specification (PEP 440) was created to support the existing versioning schemes and unify them. It is extremely detailed and full of information, but it’s also very complex and lacks clear guidance. While PEP 440 improved Python by creating uniform versioning schemes, Python’s versioning is still bizarre by today’s standards.  

Best Practice: Approximate Semantic Versioning 

Python’s versioning specifications were designed to be flexible and support a wide range of different versioning schemes. However, we recommend following a three-part versioning scheme like Semantic Versioning (SemVer).

SemVer has become the go-to versioning scheme due to the following reasons: 

  • The documentation is detailed and easy to understand, leaving little room for deviation across libraries. 
  • It helps with both dependency resolution and prerelease package labeling. 
  • It is easy to pick up and understand for both developers and relevant stakeholders.  

While Python doesn’t fully support SemVer, you can still create three-part versions in the same manner. Semantic Versioning works by structuring each version identifier into three parts, MAJOR, MINOR, and PATCH. Each of these parts is managed as a number and incremented according to the following rules: 

  • Major releases (5.0.0) indicate changes that will be incompatible with previous versions. 
  • Minor releases (5.1.0) add functionality while still being backward-compatible (in this example 5.1.0 would be compatible with 5.0.0). 
  • Patch (often termed Micro in Python) releases are minor bug fixes or security patches that should always be backward compatible (5.1.4). 

Best Practice: Build Prerelease Packages 

Packages must have unique version numbers and prerelease labels are a simple way to manage this. Unfortunately, you can’t use prerelease specifiers like you can in SemVer. Instead, Python has Pre-release Segments that behave and look similarly. A SemVer prerelease label starts with a hyphen and is a multi-part, dot-separated identifier. Meanwhile, a Python Pre-release segment consists of two parts: the phase and a number. 

  • SemVer Prerelease Package: 5.3.1-rc.1 
  • Python Prerelease Package: 5.3.1rc1 

Python Pre-release segments were designed around a specific “alpha, beta, release candidate” pre-release cycle. Most Python tools support using any phase and omitting the number, but the preferred phases are a,b, and rc.

There are also a few odd behaviors you should be aware of: 

  • When no number is given, it is converted into 0 
  • alpha is converted to a 
  • beta is converted to b 
  • c, pre, and preview are converted to rc 

In SemVer, alphabetically sortable prerelease labels that align to your CI/CD stages (-ci.4, -qa.4, -rc.4, etc) are typically used. This can be approximated in Python by using Pre-release segments like ci4, qa4, and rc4, but it’s safest to stick with the three preferred phases (a, b, and rc). 

  • a4 indicates a package produced by a CI server 
  • b4 indicates a package that’s been “promoted” and is ready for testing 
  • rc4 indicates a tested package that’s nearly ready for release 

Similar to SemVer, you should use alphabetically sortable prerelease segments that align with your CI/CD stages. For example, consider a package named “kramerica_utils” with a version number of 5.3.1. 

Using the prerelease segment b4 indicates that “kramerica_utils” is ready to be tested. 

  • kramerica_utils.5.3.1b4  

Meanwhile, rc4 indicates that “kramerica_utils” is almost ready to be released. 

  • kramerica_utils.5.3.1rc4 

Best Practice: Avoid Other Versioning Features 

Stick with three-part versions with Pre-release Segments. This is as close to SemVer as you can get. Avoid using other Python versioning features like: 

Best Practice: Don’t Deploy Prerelease Packages 

Prerelease segments let developers know if a Python package is still in development. Therefore, packages with prerelease segments should never be deployed per the SemVer standard

Any package without a prerelease label can be considered “stable”. Meanwhile, a package with a phase and number should be considered unstable and not deployable.  

  • 5.3.1rc1 = ❌
  • 5.3.1 = ✅

Best Practice: Repackage After Testing 

If you’re not deploying Python prerelease packages but continuing to build them, then you’ll need a method of turning prerelease packages into stable ones

ProGet’s repackaging feature takes a prerelease package and creates a new one with the exact same content, but a different version number.

Repackaging ensures that only tested packages end up in production. For example, to repackage 5.3.1, ProGet creates 5.3.1rc1, then after it’s tested and approved, it is repackaged as stable version 5.3.1. 

Beyond Versioning and Python 

Python package version numbers should be used for a clear purpose with rules that are followed consistently. Using the best practices outlined in this article will help manage package dependency, clearly communicate changes made with package versions, and keep untested packages from sneaking into your code. 

Versioning Python packages is really important, but there is a lot more to learn when it comes to becoming a Python expert. Read our guide to learn everything you need to know to become a Python master! 

Crista Perlton

Crista Perlton