Skip to content

Makefile and make

make is a build automation tool driven by a Makefile. In data/AI projects it is often less about compiling C and more about giving teammates a small, consistent set of commands: install, lint, test, train, clean, deploy.

A Makefile is not magic. It is a list of targets, dependencies, and shell commands, with a little syntax for variables and pattern rules. When kept small and readable, it is very effective.

Quick Reference

Pattern Meaning
target: a rule name
target: dep1 dep2 run this target if deps are newer
\tcommand recipe line, must start with a tab
.PHONY: target target is not a real file
$(VAR) or ${VAR} variable reference
VAR = value variable assignment
make target run a specific target
make -n target dry run
make -j N run with N parallel jobs

A tab in a recipe is required. Spaces do not work there, which is one of the most common first-time Makefile errors.

Smallest Useful Makefile

.PHONY: install test clean

install:
    pip install -r requirements.txt

test:
    pytest

clean:
    rm -rf .pytest_cache
    find . -type d -name "__pycache__" -exec rm -rf {} +

Usage:

make install
make test
make clean

The point is not to impress anyone with cleverness. The point is that a new contributor can run the same few commands without reading tribal knowledge.

Targets

A target is the name of something to build or do.

train:
    python train.py

eval:
    python evaluate.py

Targets can depend on other targets:

run: install test
    python main.py

Before running run, make checks whether install and test need to be re-run, based on file dependencies and timestamps.

Phony Targets

If a target does not correspond to an actual file, mark it .PHONY.

.PHONY: test lint clean

test:
    pytest

This matters because if a file named test exists, make might think the target is already up to date and do nothing.

Common phony targets: - test - lint - clean - install - dev - run

Recipes

A recipe is one or more shell commands.

format:
    black src tests
    isort src tests

Each line in a recipe runs in its own shell by default, unless you use line continuations.

dev:
    export APP_ENV=development && \
    uvicorn main:app --reload

A backslash-newline continues a single shell command across multiple Makefile lines.

Variables

Variables reduce repetition.

PYTHON = python
PIP = pip
TEST = pytest
SRC = src
TESTS = tests

.PHONY: test

test:
    $(TEST) $(TESTS)

Variable assignment forms:

A = value     # recursive expansion
A := value    # immediate expansion
A ?= value    # set only if not already set
A += value    # append

For most small project Makefiles, = or := is enough. The distinction matters more in larger build systems.

Command-Line Variables

You can override variables from the command line:

make train EPOCHS=5

Makefile:

EPOCHS ?= 10

train:
    python train.py --epochs $(EPOCHS)

This is a clean way to expose a few knobs without writing many similar targets.

A Practical Data Science Makefile

SHELL := /bin/bash

PYTHON := python
PIP := pip
TEST := pytest
LINT := ruff

.PHONY: install deps test lint format clean run dev

install:
    $(PIP) install -r requirements.txt

deps:
    $(PIP) install -r requirements-dev.txt

test:
    $(TEST)

lint:
    $(LINT) check .

format:
    $(LINT) format .

clean:
    rm -rf .pytest_cache
    rm -rf .ruff_cache
    find . -type d -name "__pycache__" -exec rm -rf {} +

run:
    $(PYTHON) main.py

dev:
    $(PYTHON) -m uvicorn main:app --reload

This is the kind of Makefile that helps a project feel organized without becoming a build-system project in itself.

Pattern Rules

Pattern rules let one rule apply to many files.

%.csv: %.parquet
    python convert.py --input $< --output $@

Here: - % matches a stem - $< is the first dependency - $@ is the target

This is more common for file-to-file workflows than for general project commands.

Automatic Variables

Useful ones: - $@ — target file - $< — first prerequisite - $^ — all prerequisites - $(@D) / $(@F) — directory or file part of target

These are most helpful in pattern rules and file-based rules.

Silencing and Debugging

.SILENT: clean

Or per-command:

clean:
    @rm -rf .pytest_cache

The @ prefix hides the command itself, showing only its output.

For debugging:

make -n test       # show commands without running them
make --debug       # more verbose make behavior
make -p            # print database of rules and variables

Common Patterns

One target per common task

.PHONY: test lint format install clean
check: lint test

.PHONY: check
check:
    $(MAKE) lint
    $(MAKE) test

Keep local overrides out of the shared file

-include .env.make

The dash avoids an error if the file is missing.

Make vs Shell Scripts

A Makefile is good when: - You want named tasks with dependencies. - You want a small project-level interface. - Multiple people should run the same commands.

A shell script is good when: - The logic is mostly sequential. - You need conditionals and loops that are easier in bash/Python. - The command is a one-off procedure.

Use the tool that matches the job. Many projects use both.

Common Mistakes

  • Using spaces instead of a tab in recipes.
  • Forgetting .PHONY for non-file targets.
  • Making the Makefile more clever than the team can read.
  • Relying on implicit rules when explicit ones are clearer.
  • Hardcoding absolute paths where a variable would be better.
  • Writing a Makefile that only works on one contributor's machine.
  • Using make for tasks that are really scripting problems.
  • Recreating the same environment-dependent logic in every recipe instead of centralizing it.

Make and Environment Variables

Makefiles do not replace environment configuration. Often the best pattern is: - Makefile runs the command. - Environment variables or config files carry deployment-specific values. - The Makefile stays generic.

Example:

run:
    python main.py

Then:

APP_ENV=staging make run

This keeps the Makefile from becoming a secret store or a deployment script.

  • python-dotenv — environment-based configuration that Make can rely on
  • curl — calling services from make-driven workflows
  • ssh — remote execution where make targets may trigger deployment or sync
  • yaml — config files often consumed by make-driven pipelines
  • toml — tool configuration that Make may install or validate

Official Documentation

  • GNU make manual, or the make documentation for your system
  • Your project's conventions, since many teams extend make in their own way

A good Makefile is the one your teammates can understand in a minute and use without asking.

Last Updated

2026-09-06 — Initial version.