Makefiles: The Unsung Hero of Python Development
I’ve been building Python libraries for years, and there’s one tool that consistently makes my life easier: the humble Makefile. Yes, that decades-old build automation tool from the C world. It might seem old school, but it’s become an essential part of my Python development workflow, and today I want to show you why.
What’s a Makefile Anyway?
If you’re coming from a pure Python background, you might be wondering what Make is and why we’re talking about it. Make is a build automation tool that’s been around since 1976. Think of it as a simple way to define shortcuts for common commands in your project. Instead of remembering and typing out long commands, you can define them once in a Makefile and run them with a simple make command
.
For example, instead of typing:
python -m pytest tests/ --cov=mypackage --cov-report=term-missing
You can just type:
make test
Why Use Make in Python Projects?
“But wait,” I hear you say, “Python already has tools for this stuff! We have tox, we have invoke, we have poetry scripts…”
You’re not wrong. But here’s why I still reach for Make:
- It’s ubiquitous: Make is installed by default on most Unix-based systems (including macOS)
- It’s language-agnostic: The same tool works across your Python, JavaScript, and other projects
- It’s simple: No need to learn yet another Python package’s API
- It’s standardized: Once you learn Make’s syntax, you can understand Makefiles in any project
Real-World Examples from Our Libraries
Let me show you how we use Makefiles in some of our projects. In our cookiecutter-pipproject template, we include a standard Makefile that sets up common commands for any Python project:
install-dev:
pip install -e ".[dev]"
test:
pytest tests/ --cov=mypackage --cov-report=term-missing
lint:
ruff check .
format:
ruff format .
docs:
sphinx-build -b html docs/source docs/build
We use this same pattern across our libraries like keeks
, elote
, and pygeohash
. The beauty is that regardless of which project I’m working on, I know that make test
will run the tests, make lint
will check the code quality, and so on. The underlying implementation might differ (some projects use pytest, others might use unittest), but the interface stays the same.
This even allows you to do more complicated things. Do your tests depend on some service that you’re running in docker-compose? No problem, throw it in the Makefile.
The Power of Consistency
This consistency is more valuable than you might think. When I’m jumping between projects (which happens a lot), I don’t need to remember:
- Does this project use ruff or black for formatting?
- What’s the pytest command with coverage reporting?
- How do I build the documentation?
I just run the same make
commands everywhere. The Makefile handles the details.
Beyond the Basics
Once you get comfortable with Make, you can do some pretty neat things. Here’s a more advanced example from one of our projects:
.PHONY: all test clean docs
VERSION := $(shell python -c "from mypackage import __version__; print(__version__)")
all: test lint docs
clean:
rm -rf build/
rm -rf dist/
rm -rf *.egg-info
find . -type d -name __pycache__ -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
release: clean
python -m build
twine upload dist/*
git tag v$(VERSION)
git push origin v$(VERSION)
This Makefile not only provides shortcuts but also ensures steps happen in the right order. The release
target cleans up old builds, creates a new distribution, uploads it to PyPI, and tags the release in git - all with a single command.
Getting Started with Make
Want to add a Makefile to your Python project? Here’s a minimal starter:
.PHONY: test lint format
test:
pytest
lint:
ruff check .
format:
ruff format .
install:
pip install -e .
Save this as Makefile
(no extension) in your project root, and you’re good to go. Remember to use tabs for indentation - Make is picky about that!
The Bottom Line
Are Makefiles perfect? No. They can be finicky about syntax, and they’re not as powerful as some modern task runners. But they’re simple, universal, and they get the job done. In a world where we’re constantly chasing the next shiny tool, sometimes the old reliable option is exactly what we need.
Plus, there’s something satisfying about using a tool that’s been solving problems since before I was born. If it ain’t broke, don’t fix it, right?
Note: If you want to see more examples of how I use Makefiles in practice, check out our cookiecutter-pipproject template or any of my Python libraries on GitHub. They all follow these patterns.
Subscribe to the Newsletter
Get the latest posts and insights delivered straight to your inbox.