Make isn't just for C projects. It's a great task runner for Python too.
Why Make?
- Universal: installed everywhere
- Simple: just shell commands
- Dependency-aware: only runs what's needed
- Self-documenting:
make helpshows available targets
Basic Structure
.PHONY: help setup test lint clean
help:
@echo "Available targets:"
@echo " setup - Create venv and install deps"
@echo " test - Run tests"
@echo " lint - Run linter"
@echo " clean - Remove artifacts"
setup:
python -m venv .venv
.venv/bin/pip install -e ".[dev]"
test:
.venv/bin/pytest tests/ -v
lint:
.venv/bin/ruff check src/ tests/
clean:
rm -rf .venv __pycache__ .pytest_cache dist/.PHONY tells Make these aren't real files.
Variables
VENV := .venv
PYTHON := $(VENV)/bin/python
PIP := $(VENV)/bin/pip
PYTEST := $(VENV)/bin/pytest
RUFF := $(VENV)/bin/ruff
test:
$(PYTEST) tests/ -v
lint:
$(RUFF) check src/Change one variable, update everywhere.
Common Targets
.PHONY: setup dev test lint format check clean build publish
# Setup
setup: $(VENV)/bin/activate
$(VENV)/bin/activate:
python -m venv $(VENV)
$(PIP) install -e ".[dev]"
touch $@
# Development
dev:
$(PYTHON) -m myapp --debug
# Testing
test:
$(PYTEST) tests/ -v
test-cov:
$(PYTEST) tests/ --cov=src --cov-report=html
# Code quality
lint:
$(RUFF) check src/ tests/
format:
$(RUFF) format src/ tests/
typecheck:
$(PYTHON) -m mypy src/
check: lint typecheck test
# Cleanup
clean:
rm -rf $(VENV) dist/ build/ *.egg-info
find . -type d -name __pycache__ -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
# Build & Publish
build:
$(PYTHON) -m build
publish: build
$(PYTHON) -m twine upload dist/*Dependency Tracking
Make can track file dependencies:
# Only reinstall if pyproject.toml changes
$(VENV)/bin/activate: pyproject.toml
python -m venv $(VENV)
$(PIP) install -e ".[dev]"
touch $@
# Only rebuild docs if source changes
docs/index.html: docs/*.md
$(PYTHON) -m mkdocs buildConditional Logic
# Detect OS
ifeq ($(OS),Windows_NT)
PYTHON := $(VENV)/Scripts/python
else
PYTHON := $(VENV)/bin/python
endif
# Use different settings for CI
ifdef CI
PYTEST_ARGS := --no-header -q
else
PYTEST_ARGS := -v
endif
test:
$(PYTEST) tests/ $(PYTEST_ARGS)Self-Documenting Help
.DEFAULT_GOAL := help
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " %-15s %s\n", $$1, $$2}'
setup: ## Create venv and install dependencies
python -m venv .venv
.venv/bin/pip install -e ".[dev]"
test: ## Run tests
.venv/bin/pytest tests/ -v
lint: ## Run linter
.venv/bin/ruff check src/$ make
help Show this help
setup Create venv and install dependencies
test Run tests
lint Run linterMy Standard Makefile
.PHONY: help setup test lint format check clean
.DEFAULT_GOAL := help
VENV := .venv
PYTHON := $(VENV)/bin/python
PIP := $(VENV)/bin/pip
help: ## Show available commands
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
setup: $(VENV)/bin/activate ## Create venv and install deps
$(VENV)/bin/activate: pyproject.toml
python -m venv $(VENV)
$(PIP) install --upgrade pip
$(PIP) install -e ".[dev]"
touch $@
test: ## Run tests
$(PYTHON) -m pytest tests/ -v
lint: ## Run linter
$(PYTHON) -m ruff check src/ tests/
format: ## Format code
$(PYTHON) -m ruff format src/ tests/
check: lint test ## Run all checks
clean: ## Remove build artifacts
rm -rf $(VENV) dist/ build/ *.egg-info .pytest_cache .ruff_cache
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || trueTips
Always use .PHONY for targets that aren't files.
Use @ prefix to hide the command being run.
Use $(MAKE) for recursive Make calls.
Keep it simple. If a target gets complex, move it to a script.
Make is old but effective. Learn it once, use it everywhere.
React to this post: