🎉 I'm releasing 12 products in 12 months! If you love product, checkout my new blog workingoutloud.dev

Back to home

Semantic Versioning In Python With Git Hooks

This is Day 22 of the #100DaysOfPython challenge.

This post will use the pre-commit and commitizen packages to automate our semantic versioning for Python based on our commits.

The post will build off the work done in the post "Your First Pip Package With Python".

The final code can be found here.


  1. Familiarity with Pipenv. See here for my post on Pipenv.

Getting started

We are going to clone and work from the okeeffed/your-first-pip-package-in-python repository and install the required packages.

$ git clone https://github.com/okeeffed/your-first-pip-package-in-python semantic-versioning-in-python-with-git-hooks $ cd semantic-versioning-in-python-with-git-hooks # Init the virtual environment $ pipenv install --dev pre-commit Commitizen toml # Create some required files $ touch .pre-commit-config.yaml

At this stage, we are ready to configure our pre-commit hook.

Pre-commit configuration

We need to add the following to .pre-commit-config.yaml:

--- repos: - repo: https://github.com/commitizen-tools/commitizen rev: master hooks: - id: commitizen stages: [commit-msg]

Once done, we can setup our Commitizen configuration.

Setting up Commitizen

We can set this up by running pipenv run cz init:

$ pipenv run cz init ? Please choose a supported config file: (default: pyproject.toml) pyproject.toml ? Please choose a cz (commit rule): (default: cz_conventional_commits) cz_conventional_commits No Existing Tag. Set tag to v0.0.1 ? Please enter the correct version format: (default: "$version") ? Do you want to install pre-commit hook? Yes commitizen already in pre-commit config commitizen pre-commit hook is now installed in your '.git' You can bump the version and create changelog running: cz bump --changelog The configuration are all set.

We are now at a stage where our commits can affect our versioning! A file is created for us pyproject.toml:

[tool] [tool.commitizen] name = "cz_conventional_commits" version = "0.0.1" tag_format = "$version"

This will contain the version information that is maintained by Commitizen for us.

Automated semantic versioning at work

First of all, we will need to create a tag for version 0.0.1:

$ git tag -a 0.0.1 -m "Init version"

Our version will change based on the naming that we give our commits. The installed Git hook will enforce that our commits follow the Conventional Commits naming conventions.

Let's see this in action. First, let's add a new function to the file demo_pip_math/math.py:

def divide(x: int, y: int) -> float: """divide one number by another Args: x (int): first number in the division y (int): second number in the division Returns: int: division of x and y """ return x / y

We want to add and commit this code. Let's see what happens when we set an invalid version:

$ git add demo_pip_math/math.py $ git commit -m "added new feature division" commitizen check.........................................................Failed - hook id: commitizen - exit code: 14 commit validation: failed! please enter a commit message in the commitizen format. commit "": "not a valid commit name" pattern: (build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert|bump)(\(\S+\))?!?:(\s.*)

This tells use that we did not match the expected pattern.

Given that we are adding a new feature, let's try with the feat: prefix:

$ git commit -m "feat: added new divide functionality" commitizen check.........................................................Passed [main 333d291] feat: added new divide functionality 1 file changed, 13 insertions(+)

We've passed!

Now that we've made a change, we can use Commitizen to help us update our version and create a Changelog and update the version:

$ pipenv run cz bump bump: version 0.0.1 → 0.1.0 tag to create: 0.1.0 increment detected: MINOR Done!

If we now check pyproject.toml, we can see that change reflected for us:

[tool] [tool.commitizen] name = "cz_conventional_commits" version = "0.1.0" tag_format = "$version"

Creating the Changelog

To create a Changelog, run pipenv run cz changelog:

$ pipenv run cz changelog # ... no output

This will create a CHANGELOG.md file in our project root directory, now with the following information:

## 0.1.0 (2021-08-10) ### Feat - added new divide functionality <Adsense /> ## 0.0.1 (2021-08-10) ### Feat - add in division function - init commit


Keeping setup.py in sync

Finally, we want to make sure that our setup.py is in sync with what is reflected in pyproject.toml.

We can do this with the toml package.

Inside of setup.py, change the file to be the following:

import setuptools import toml from os.path import join, dirname, abspath pyproject_path = join(dirname(abspath("__file__")), '../pyproject.toml') file = open(pyproject_path, "r") toml_str = file.read() parsed_toml = toml.loads(toml_str) with open("README.md", "r") as fh: long_description = fh.read() setuptools.setup( name="demo_pip_math", version=parsed_toml['tool']['commitizen']['version'], author="Dennis O'Keeffe", author_email="hello@dennisokeeffe.com", description="Demo your first Pip package.", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/okeeffed/your-first-pip-package-in-python", packages=setuptools.find_packages(), classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], python_requires='>=3.6', keywords='pip-demo math', project_urls={ 'Homepage': 'https://github.com/okeeffed/your-first-pip-package-in-python', }, )

We are reading in the TOML and finding the reference to the current version.

We can check that this still works as expected with a build script that was already added in the repo:

$ pipenv run build running sdist running egg_info creating demo_pip_math.egg-info writing demo_pip_math.egg-info/PKG-INFO writing dependency_links to demo_pip_math.egg-info/dependency_links.txt writing top-level names to demo_pip_math.egg-info/top_level.txt writing manifest file 'demo_pip_math.egg-info/SOURCES.txt' reading manifest file 'demo_pip_math.egg-info/SOURCES.txt' reading manifest template 'MANIFEST.in' adding license file 'LICENSE' writing manifest file 'demo_pip_math.egg-info/SOURCES.txt' running check creating demo_pip_math-0.1.0 creating demo_pip_math-0.1.0/demo_pip_math creating demo_pip_math-0.1.0/demo_pip_math.egg-info creating demo_pip_math-0.1.0/tests copying files to demo_pip_math-0.1.0... copying LICENSE -> demo_pip_math-0.1.0 copying MANIFEST.in -> demo_pip_math-0.1.0 copying README.md -> demo_pip_math-0.1.0 copying pyproject.toml -> demo_pip_math-0.1.0 copying setup.py -> demo_pip_math-0.1.0 copying demo_pip_math/__init__.py -> demo_pip_math-0.1.0/demo_pip_math copying demo_pip_math/math.py -> demo_pip_math-0.1.0/demo_pip_math copying demo_pip_math.egg-info/PKG-INFO -> demo_pip_math-0.1.0/demo_pip_math.egg-info copying demo_pip_math.egg-info/SOURCES.txt -> demo_pip_math-0.1.0/demo_pip_math.egg-info copying demo_pip_math.egg-info/dependency_links.txt -> demo_pip_math-0.1.0/demo_pip_math.egg-info copying demo_pip_math.egg-info/top_level.txt -> demo_pip_math-0.1.0/demo_pip_math.egg-info copying tests/__init__.py -> demo_pip_math-0.1.0/tests copying tests/test_math.py -> demo_pip_math-0.1.0/tests Writing demo_pip_math-0.1.0/setup.cfg creating dist Creating tar archive removing 'demo_pip_math-0.1.0' (and everything under it) running bdist_wheel running build running build_py creating build creating build/lib creating build/lib/demo_pip_math copying demo_pip_math/__init__.py -> build/lib/demo_pip_math copying demo_pip_math/math.py -> build/lib/demo_pip_math creating build/lib/tests copying tests/__init__.py -> build/lib/tests copying tests/test_math.py -> build/lib/tests warning: build_py: byte-compiling is disabled, skipping. installing to build/bdist.macosx-11-x86_64/wheel running install running install_lib creating build/bdist.macosx-11-x86_64 creating build/bdist.macosx-11-x86_64/wheel creating build/bdist.macosx-11-x86_64/wheel/demo_pip_math copying build/lib/demo_pip_math/__init__.py -> build/bdist.macosx-11-x86_64/wheel/demo_pip_math copying build/lib/demo_pip_math/math.py -> build/bdist.macosx-11-x86_64/wheel/demo_pip_math creating build/bdist.macosx-11-x86_64/wheel/tests copying build/lib/tests/__init__.py -> build/bdist.macosx-11-x86_64/wheel/tests copying build/lib/tests/test_math.py -> build/bdist.macosx-11-x86_64/wheel/tests warning: install_lib: byte-compiling is disabled, skipping. running install_egg_info Copying demo_pip_math.egg-info to build/bdist.macosx-11-x86_64/wheel/demo_pip_math-0.1.0-py3.9.egg-info running install_scripts adding license file "LICENSE" (matched pattern "LICEN[CS]E*") creating build/bdist.macosx-11-x86_64/wheel/demo_pip_math-0.1.0.dist-info/WHEEL creating 'dist/demo_pip_math-0.1.0-py3-none-any.whl' and adding 'build/bdist.macosx-11-x86_64/wheel' to it adding 'demo_pip_math/__init__.py' adding 'demo_pip_math/math.py' adding 'tests/__init__.py' adding 'tests/test_math.py' adding 'demo_pip_math-0.1.0.dist-info/LICENSE' adding 'demo_pip_math-0.1.0.dist-info/METADATA' adding 'demo_pip_math-0.1.0.dist-info/WHEEL' adding 'demo_pip_math-0.1.0.dist-info/top_level.txt' adding 'demo_pip_math-0.1.0.dist-info/RECORD' removing build/bdist.macosx-11-x86_64/wheel

We can see that version 0.1.0 is used in this build.


Today's post demonstrated how to use the commitizen, pre-commit and toml packages to help automate our versioning process based on Conventional Commits.

This helps larger teams keep their packages semantically correct with little effort added to their work.

Resources and further reading

Photo credit: snapsbyclark

Personal image

Dennis O'Keeffe

  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on Visibuild.


Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.

Semantic Versioning In Python With Git Hooks


Share this post