Old Hat, New Tricks: Makefiles for Python Projects
Table of Contents
Makefiles have been a staple of the C/C++ toolchain for decades. In a nutshell:
GNU Make executes discrete steps that are defined in a Makefile. The steps
defined in a Makefile can have dependencies, so that when a step is executed,
all the dependent steps are executed first. This enables effective version
control and cleaner documentation for the manual steps that are needed to build
and run a project.
Like many GNU utilities, it’s been around for a while and is easily overlooked by developers until they encounter a project that uses them effectively. I recently had that experience in some recent Python repositories I’ve been working in. At first I was skeptical, but there are many benefits to using them in modern projects that I didn’t previously consider:
- Less documentation to write and maintain
- Dependencies between steps are built-in to the Makefile
- Just a brief summary of
Makefilesteps for your project will suffice
Previously I used a stack of bash scripts to achieve the same goal. But I found
that there’s a lot of careful organisation needed to avoid user error caused by
executing steps in an incorrect order. Now I find that with using the
Makefile, much shorter documentation is needed – as all the details and the
dependencies between steps are handled in the construction of the Makefile.
The steps in make can also execute any shell script, so steps can still be
written in scripts and orchestrated with make.
One sneaky “gotcha” with writing a Makefile is that the steps require
indentation with one tab character, not spaces (I found
this blog post,
which contains an email from the author of make who regrets this decision
with good humor!). I use this syntax file to handle this automatically when
writing a Makefile in vim:
" ~/.vim/ftplugin/make.vim
set noexpandtab shiftwidth=8 softtabstop=0So why is it useful in Python? There are so many ways to manage dependencies in
Python (and of course, none of them quite meet the 100% mark). Now instead of
meticulously documenting how to use the tool of choice for a project, I
simply document which tool is needed and let the Makefile contain all the
details.
For example, here’s a Makefile I would write for a Python project that uses
poetry for dependency management, pytest for testing, ruff for linting
and formatting, and docker for building images:
SHELL := /usr/bin/env bash
.SHELLFLAGS := -eu -o pipefail -c
.PHONY: _check-python-version, install-uv, install-py-deps, install-py-deps-dev, check-tests, check-lint, check-format, check-all, build-image, build-image-dev, load-dev-shell
_check-python-version:
@version=$$(python --version 2>&1); \
if [ "$$version" != "Python 3.13.7" ]; then \
echo "Error: Python version is not 3.12.7. Detected version: $$version"; \
exit 1; \
fi
install-poetry: _check-python-version
@brew install pipx
@pipx install poetry==1.8.4 --python=$(which python)
install-py-deps:
@poetry install --sync --only=main
install-py-deps-dev:
@poetry install --sync --only=main,dev
check-lint:
@poetry run pytest
check-lint:
@poetry run ruff check
check-format:
@poetry run ruff format --check .
check-all: check-tests check-lint check-format
build-image-base:
@docker build -t "my-container-base" -f Dockerfile.base .
build-image-dev: build-image
@docker build -t "my-container-dev" -f Dockerfile.dev .
load-dev-shell: build-dev-image
@docker exec -it "my-container-dev" bash
The targets in .PHONY ensure that the steps execute every time they’re
called, and @ suppresses the command output. Using this file, all my steps
are version controlled, unwieldy flags and options for commands don’t need to
be copy-pasted, and users with brew can get up and running with a few simple
commands:
$ make install-poetry
$ make install-py-deps-dev
$ make check-all
$ make load-dev-shellThe Makefile is here to stay. It’s simplified my development process, and
it’s now one of the first things I’ll write for a new project
References:
#productivity #automation #software-engineering #programming #devops-practices #makefile #dependency-management #python #build-automation #gnu-make #project-organization #build-tools #makefiles
Reply to this post by email blZake@proZbableodyssey.blog (remove Z characters) ↪
Comments
Leave a comment
Markdown is supported. Your email is private and only used if you'd like a reply.