Skip to Content
Course content

347: Semantic Versioning for Python Packages

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was maintaining a production data pipeline that relied on a small, third-party utility library for parsing specialized telemetry files. One Tuesday morning, our CI/CD pipeline started failing across the board. I checked the logs and saw that a dependency had been updated. The library maintainer had pushed a "small update" from version 1.4.2 to 1.5.0. On the surface, it looked like a minor feature addition, but they had actually changed a core function's return type from a list to a generator. Because my code called .sort() on that return value, the entire pipeline crashed. I spent four hours debugging a "minor" update that should have been a major version bump.

That is the exact pain point that Semantic Versioning (SemVer) is designed to solve. When you're writing a script for yourself, the version doesn't matter. But the moment you publish a package to PyPI or share a library across teams, your version number becomes a contract. It tells your users exactly how much risk they are taking when they run pip install --upgrade.

The Logic of Major, Minor, and Patch

SemVer follows a strict MAJOR.MINOR.PATCH format (e.g., 2.4.12). It sounds simple, but the discipline lies in knowing when to increment which number. If you get this wrong, you're essentially lying to your users about the stability of your code.

  • MAJOR: You increment this when you make incompatible API changes. If you rename a public function, remove a parameter, or change a return type (like the generator disaster I mentioned), you must bump the Major version. This warns the user: "Stop! This update might break your code."
  • MINOR: You increment this when you add functionality in a backwards-compatible manner. You've added a new feature or a new optional argument to a function, but the existing code will still work exactly as it did before.
  • PATCH: This is for backwards-compatible bug fixes. No new features, no broken APIs—just fixing a typo in a docstring or squashing a logic bug that was causing an incorrect calculation.

One quick aside: if your project is in early development and the API is changing every hour, it's common to stay in the 0.x.y phase. In the 0.x world, the rules are looser, and anything can change at any time. But once you hit 1.0.0, you've signaled that the project is stable and the SemVer contract is officially active.

Applying Versioning to Python Packaging

In Python, you typically define your version in one of two places: your __init__.py file or your pyproject.toml. I personally prefer defining it in the __init__.py so that users can check the version at runtime using your_package.__version__.

# my_library/__init__.py
__version__ = "1.2.0"

However, the real power of SemVer comes when your users define their dependencies in a requirements.txt or pyproject.toml file. If they trust your versioning, they won't just pin your library to a specific version (like my_library == 1.2.0), which is a maintenance nightmare. Instead, they can use "compatible release" specifiers.

For example, if a user writes my_library ~= 1.2.0, pip will install the latest patch version (e.g., 1.2.5), but it will never install 1.3.0 or 2.0.0. This allows them to get your bug fixes automatically without risking a breaking change. If you've followed SemVer, you've just made your library a joy to use.

The Role of the Changelog

A version number is a signal, but a changelog is the explanation. I've seen plenty of developers bump a version to 2.0.0 and leave the users guessing what actually broke. Your changelog should be a chronological record tied directly to your SemVer bumps. Instead of saying "fixed some stuff," be explicit: "Changed parse_data() to return a generator instead of a list to improve memory efficiency."

When you pair a Major bump with a clear explanation of the breaking change, you shift the burden of stability from the user's guesswork to your professional documentation.




📋 Practical Task

Updating the 'WeatherSync' Library Versioning

You are the maintainer of a Python library called WeatherSync, currently at version 2.4.1. You have three pending changes that need to be released. Based on the principles of Semantic Versioning, determine the correct new version number for each release and explain your reasoning.

The Scenario:

  • Change A: You found a bug where the get_temp() function returned Celsius instead of Fahrenheit in one specific edge case. You fixed the logic, and the function signature remains identical.
  • Change B: You added a brand new feature, get_humidity(), which allows users to fetch humidity data. All existing functions still work exactly as they did before.
  • Change C: You realized the fetch_all() function was inefficient. You decided to remove it entirely and replace it with fetch_by_region(), which requires a new region_id argument. Any code using fetch_all() will now crash.

Your Task:

  1. If you released Change A alone, what would the new version be?
  2. If you released Change B alone, what would the new version be?
  3. If you released Change C alone, what would the new version be?
  4. If you bundled all three changes into a single release, what would the final version number be?

Write your answers as a list of version numbers with a one-sentence justification for each.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.