Django Dependency Management with pip-compile and pip-tools

Updated

Table of Contents

Even a basic Django project has multiple dependencies. After installing Django itself, various third-party packages--and their dependencies!--must be installed and managed. How do you track version numbers in a reproducible way?

In the JavaScript ecosystem, the npm package manager makes it easy to keep track of requirements. Every time a package is installed via npm install <package-name>, an additional file, package.json, is created that is automatically updated with the package name and version number.

Unfortunately, the Python package manager, pip, does not do that, so other approaches are required. In this post, we'll run through several options, including pip freeze and requirements.txt, and then demonstrate why pip-compile is a popular approach for many Django developers.

Django Set Up

Let's quickly go through the standard setup for a new Django project. Navigate to a new directory on the command line, create a fresh virtual environment, and install Django.

# Windows
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $ python -m pip install django~=4.2.0

# macOS
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ python3 -m pip install django~=4.2.0

Even at this introductory phase, we've installed Django and some of its dependencies.

pip freeze and requirements.txt

The command pip freeze lists all installed packages within a virtual environment and their version numbers. Let's try it now.

(.venv) $ pip freeze
asgiref==3.7.2
Django==4.2.4
sqlparse==0.4.4

The latest version of Django, 4.2.4, relies on two other packages: asgiref and sqlparse.

You can output this to a physical file, typically called requirements.txt using the following command:

(.venv) $ pip freeze > requirements.txt

This file can be stored in source control using git. A fellow developer can download the code, create a fresh virtual environment, and run a command to install all the packages into their local environment.

(.venv) $ pip install -r requirements.txt

But this manual process requires the developer to regenerate a requirements.txt file every time a package is updated. It also doesn't directly address package dependencies.

pip-tools

A more sophisticated approach is using pip-tools, a command line tool for managing Python project dependencies. Let's install it and then run pip freeze to confirm it is installed correctly.

(.venv) $ python -m pip install pip-tools
(.venv) $ pip freeze
asgiref==3.7.2
build==0.10.0
click==8.1.7
Django==4.2.4
packaging==23.1
pip-tools==7.3.0
pyproject_hooks==1.0.0
sqlparse==0.4.4

Whoa, suddenly there are a lot more package dependencies to manage! Using our previous method, we would need to store these new packages manually in requirements.txt.

(.venv) $ pip freeze > requirements.txt

But at a high level, we only have two packages in our project: Django and pip-tools. All the other packages are dependencies of those two. With pip-tools, we can create a new root-level file called requirements.in to manage high-level dependencies. When compiled, it will automatically generate the full list of all dependencies into a requirements.txt file. Let's see this in action.

Since this tutorial is for demonstration purposes, use the deactivate command to exit the virtual environment. Delete the requirements.txt file using your text editor. Then delete the .venv directory. Note that you should always take great care when using the commands rm -rf together because you could inadvertently delete your entire computer if you're not careful. In this instance, we tell our computer to delete the entire .venv directory and its contents.

(.venv) $ deactivate
$ rm -rf .venv

Now, create a fresh virtual environment and activate it. Then install pip-tools.

# Windows
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $ python -m pip install pip-tools

# macOS
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ python -m pip install pip-tools

If you run pip freeze, you can see that pip-tools and all its dependencies are now installed in our virtual environment.

(.venv) $ pip freeze 
build==0.10.0
click==8.1.7
packaging==23.1
pip-tools==7.3.0
pyproject_hooks==1.0.0

Rather than use pip to install Django, we will add it to the requirements.in file. Here is the current project layout.

.venv/
├── requirements.in

And we only need one line in the requirements.in file since we only install one high-level package.

# requirements.in
django

To install Django and its dependencies into our virtual environment as well as a new requirements.txt file, run the following command:

(.venv) $ pip-compile requirements.in --strip-extras
#
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
#    pip-compile --strip-extras requirements.in
#
asgiref==3.7.2
    # via django
django==4.2.4
    # via -r requirements.in
sqlparse==0.4.4
    # via django

The command outputs the contents of the virtual environment and automatically adds it to the requirements.txt file for us.

# requirements.txt
#
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
#    pip-compile --strip-extras requirements.in
#
asgiref==3.7.2
    # via django
django==4.2.4
    # via -r requirements.in
sqlparse==0.4.4
    # via django

Add another package: environs

To see how pip-compile shines, try adding another package. Let's say it's environs, a way to implement environment variables that also comes with handy Django defaults.

Add it to the requirements.in file.

# requirements.in
django
environs[django]

Then, run the command pip-compile requirements.in --strip-extras to install it into the virtual environment.

(.venv) $ pip-compile requirements.in --strip-extras
#
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
#    pip-compile --strip-extras requirements.in
#
asgiref==3.7.2
    # via django
dj-database-url==2.1.0
    # via environs
dj-email-url==1.0.6
    # via environs
django==4.2.4
    # via
    #   -r requirements.in
    #   dj-database-url
django-cache-url==3.4.4
    # via environs
environs==9.5.0
    # via -r requirements.in
marshmallow==3.20.1
    # via environs
packaging==23.1
    # via marshmallow
python-dotenv==1.0.0
    # via environs
sqlparse==0.4.4
    # via django
typing-extensions==4.7.1

The output is also now present in the requirements.txt file. And a good thing, too, because environs[django] has a bunch of dependencies: dj-database-url, dj-email-url, django-cache-url, marshmallow, and python-dotenv. Note that marshmallow has its own dependency, packaging, as does dj-database-url, which relies on typing-extensions.

# requirements.txt
#
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
#    pip-compile --strip-extras requirements.in
#
asgiref==3.7.2
    # via django
dj-database-url==2.1.0
    # via environs
dj-email-url==1.0.6
    # via environs
django==4.2.4
    # via
    #   -r requirements.in
    #   dj-database-url
django-cache-url==3.4.4
    # via environs
environs==9.5.0
    # via -r requirements.in
marshmallow==3.20.1
    # via environs
packaging==23.1
    # via marshmallow
python-dotenv==1.0.0
    # via environs
sqlparse==0.4.4
    # via django
typing-extensions==4.7.1
    # via dj-database-url

We've only installed two high-level packages at this point--Django and environs[django]--yet already managing our dependencies feels tricky. Thanks to pip-compile it doesn't have to be!

Updates

By default, pip-compile generates a requirements.txt file using the latest versions for the high-level packages in requirements.in. If an existing requirements.txt fulfills a dependency, no change will be made even if updates are available.

It is up to the developer to periodically update the packages. You can do this simultaneously with pip-compile --upgrade or update a specific package using --upgrade-package.

# only update the django package
(.venv) $ pip-compile --upgrade-package django

# update all packages
(.venv) $ pip-compile --upgrade

See the pip-tools docs for complete instructions.