Python and Make in 2025
Saturday, Jul 12, 2025

I’ve been a huge fan of GNU Make for quite some time. I use it as a top-level orchestration and automation tool for managing all my projects, no matter the size.

I’ve written before about how I use Make, but in this post I’ll highlight a few key improvements to the workflow that make it much more lightweight and enjoyable to use.

Briefly: Why layer another tool on top?

Two main benefits:

  • The tool is both documentation and automation
  • The tool operates a layer above your project-specific workflow

I’ve got my own reasons for preferring Make, but the automation benefit applies to any similar tool (e.g. just). By writing a Makefile, I am documenting what things I need to be able to do on the project, as well as how to do them.

For example, I was working on a one-off project with H2O Wave (don’t ask) earlier in the year, and then had to come back a month later to fix a bug. Running make help helpfully tells me that I can do make run, and looking at the Makefile tells me that it will execute wave run [app name] [all my args].

Using Make also allows me to automate meta-management of my project. Because it operates at a layer above my virtual environment tooling, I can use a single command to trash and recreate an existing .venv/, or easily get set up for the first time on a new machine, including ensuring that I’ve got the right Python interpreter installed.

What’s changed about my workflow in 2025

I’ve already written a a post in early 2024 on using Make, but two things have changed since then:

  1. I’ve drastically simplified how I write my Makefiles
  2. I use uv for all interpreter and venv management

First, simplifying the Makefiles was really key. Trying to manage complex cross-platform projects gets really painful, really fast. By avoiding fancy help rules and pushing complex project automation down to other tooling, I can use Make as my sole interface and entrypoint for managing a project.

Second, switching to uv has been an enormous quality of life improvement. It’s seriously blazing fast, but it also strives to be pip compatible. Having previously been burned by going all-in poetry, which has its own way of defining your project and how you build your wheels, I was wary of getting locked into something that would lock me into its own way of doing things. But with uv I can simply replace something like uv pip install '.[dev]' with pip install '.[dev]' if I ever need to, and everything just works.

Lastly, uv has the ability to create virtual environments and manage Python interpreters. I can use uv venv --seed --managed-python --python 3.10 on any platform, and uv will create a .venv/ directory with Python 3.10, installing a uv-managed Python 3.10 interpreter on my system if needed.

My workflow

Prerequisites

These tools must be installed at the system level in order to support this workflow. They can all be installed via any standard package manager (including choco on Windows!), and I usually need them installed before I can do any real work, anyway:

  • Make (duh)
  • Ripgrep
  • uv

The Makefile

This is the starting point for any new project. Even if I add more rules later, these 4 rules usually remain as you see them here.

PY=3.10

help:
	@rg -oN --color=never '^[^: 	]+' Makefile

.venv:
	@uv venv --seed --managed-python --python $(PY)

install: .venv
	@uv pip install -e '.[dev]'

destroy:
	@uvx python -c "import shutil; shutil.rmtree('.venv', ignore_errors=True)"

Quick note: It’s probably not obvious when this page is rendered to HTML, but the regex query includes a space and tab character inside the square brackets.

FAQ

How do you capture complex project automation in a cross-platform way?

It’s not uncommon for large projects to have complex automation workflows. Trying to capture complex logic in a Makefile and make it work across operating systems is a sure path to insanity. I delegate that work to another tool that Make can manage. For Python projects, that tool is invoke.

For example, packaging a Python wheel for one project involves the following:

  • Binary compilation of several sub-packages with Nuitka
  • Generating .pyi files with mypy’s stubgen
  • Including a collection of static files

This is a complex, multi-step workflow across several different locations on the filesystem, but I can capture that logic in Invoke’s tasks.py file. My Makefile then simple needs a build rule that calls invoke build.