A Makefile for Python/Flask web apps
When I work on a Flask application, I need to be able to do the following:
- create a virtual environment
- activate the virtual environment
- install dependencies
- start the application
- deactivate the virtual environment
None of the actions are complicated, but I want to type less whenever possible. I'd also like the package isolation benefits of a virtual environment without having to think about activating it in different terminals or project directories.
For all this, I turn to Make. It lets me create commands that are shorter, easier to remember, and local to the project1.
Getting started
First, we create a file called Makefile with one target:
hello:
echo hello
echo world
Targets generally look like:
<target>: <dependency_1> ... <dependency_n>
<command_1>
...
<command_n>
- Dependencies are listed on the same line as the target name and run before the commands
- Commands are listed on new lines starting with a TAB character
- Targets can have 0 or more dependencies
- Targets can have 0 or more commands
If we run the hello target, we should see the two echo command results:
$ make hello
echo hello
hello
echo world
world
Silencing the output
Let's run that target again:
$ make hello
echo hello
hello
echo world
world
Notice that each command is printed (echoed) first, and then executed. It's not a big deal in this example, but the console would be very crowded when running targets that have a lot more than just two commands.
If we add an empty .SILENT target at the top of the file:
.SILENT:
hello:
echo hello
echo world
The echoing is suppressed and we only see the commands' results:
$ make hello
hello
world
Preventing default runs
Here's another bit of housekeeping before we really get going.
If you run make without a target2, it will run the file's first target:
$ make
hello
world
$ make hello
hello
world
This is fine for now, but could be a problem if the first target is a destructive action (like cleaning a directory).
To prevent this, we add an empty default target:
.SILENT:
default:
hello:
echo hello
echo world
And now running make on its own does nothing:
$ make
$ make hello
hello
world
Working with Flask
Writing the application
Our application will be an API server with a GET /hello endpoint.
First, we create a requirements.txt file for the dependencies:
Flask==3.1.0
Then, app.py for the code:
from flask import Flask, jsonify
app = Flask(__name__)
@app.get("/hello")
def hello():
return jsonify({ "msg": "Hello, World!" })
Now we're ready to add Make targets to facilitate the development process.
Creating the virtual environment
venv:
echo "Creating '.venv' python virtual environment..."
python3 -m venv .venv
This creates a virtual environment named .venv, if it doesn't already exist:
$ make venv
Installing dependencies
install:
echo "Installing python packages..."
. .venv/bin/activate
pip install -r requirements.txt
This activates the virtual environment and installs the packages listed in requirements.txt.
But before running it, we need to know more about how Make runs a target's commands. The default behavior is that each one runs in a separate shell, so make install will:
- Echo a message in shell A
- Activate the virtual environment in shell B
- Install the dependencies in shell C, but without an active virtual environment, they will be installed in the base environment instead
To fix this, we add an empty .ONESHELL target at the top of the file:
.ONESHELL:
.SILENT:
That tells Make to run a target's commands in the same shell.
Now we can run the target and see the correct ./.venv/* installation directory:
$ make install
Creating '.venv' python virtual environment...
Requirement already satisfied: Flask==3.1.0 in ./.venv/lib/python3.10/site-packages (from -r requirements.txt (line 1)) (3.1.0)
Notice that there's no
(.venv)at the end of the console output to indicate that the virtual environment is active.This is what we normally expect:
$ . .venv/bin/activate (.venv) $ echo inside inside (.venv) $ deactivate $ echo outside outsideThe reason why it's missing is that the virtual environment is not active. We activated it from within the
installtarget, which Make runs in a shell that closes once the target stops running. This means that targets written this way will run Python commands inside a virtual environment that Make will deactivate for us.
Running the application
run:
echo "Running the app..."
. .venv/bin/activate
flask --app app run
Similarly to install, this target activates the virtual environment and then runs the Flask application:
$ make run
$ curl http://localhost:5000/hello
{"msg":"Hello, World!"}
And that's about it! A few Make targets covering my regular Flask actions.
If this isn't enough, we can always add or update targets to get more functionality.
More changes to the Makefile
Initializing the project
Here's an example of adding dependencies to a target:
setup: venv install
Remember that a target run its dependencies before its commands. There are no commands here and both dependencies are targets we created earlier, so running this target is equivalent to:
$ make venv
$ make install
In other words, it's a shortcut for creating the virtual environment and installing dependencies:
$ make setup
Deleting the virtual environment
clean:
echo "Deleting '.venv' python virtual environment..."
rm -rf .venv
This deletes the .venv virtual environment, if it exists:
$ make clean
Calling make install will install or update packages based on changes to requirements.txt, but it won't delete any, so you can make clean and then make setup to delete packages you don't need anymore.
Command aliases
The install and run targets both begin by activating the virtual environment, so this appears twice in the Makefile:
. .venv/bin/activate
And we'd have to keep duplicating it for every new target we want to run inside the virtual environment. But we can create a variable to use as an alias instead.
Add this variable before the first target:
ACTIVATE_VENV := . .venv/bin/activate
And then substitute the alias for the activation commands in the targets:
install:
echo "Installing python packages..."
$(ACTIVATE_VENV)
pip install -r requirements.txt
run:
echo "Running the app..."
$(ACTIVATE_VENV)
flask --app app run
Conclusion
I went over how I write a basic Makefile for Flask projects, but it can easily adapt to another Python framework or a different language altogether. Make is a good solution to automate building, compiling, dependency management (as I've shown), or even running your own tasks inside CI tools.
And I've found that a project is easier to approach after replacing its common workflows with Make — I know I'd rather see make setup in a README than its full commands and a virtual environment explanation/reminder.
Full example
Here's the complete Makefile:
.ONESHELL:
.SILENT:
ACTIVATE_VENV := . .venv/bin/activate
default:
cat Makefile
setup: venv install
clean:
echo "Deleting '.venv' python virtual environment..."
rm -rf .venv
venv:
echo "Creating '.venv' python virtual environment..."
python3 -m venv .venv
install:
echo "Installing python packages..."
$(ACTIVATE_VENV)
pip install -r requirements.txt
run:
echo "Running the app..."
$(ACTIVATE_VENV)
flask --app app run