Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: Working With Pipenv
Pipenv is a packaging tool for Python that solves some common problems associated with the typical workflow using pip, virtualenv, and the good old requirements.txt.
In addition to addressing some common issues, it consolidates and simplifies the development process to a single command line tool.
This guide will go over what problems Pipenv solves and how to manage your Python dependencies with Pipenv. Additionally, it will cover how Pipenv fits in with previous methods for package distribution.
Free Bonus: Click here to get access to a free 5-day class that shows you how to avoid common dependency management issues with tools like Pip, PyPI, Virtualenv, and requirements files.
Problems that Pipenv Solves
To understand the benefits of Pipenv, itβs important to walk through the current methods for packaging and dependency management in Python.
Letβs start with a typical situation of handling third-party packages. Weβll then build our way towards deploying a complete Python application.
Dependency Management with requirements.txt
Imagine youβre working on a Python project that uses a third-party package like flask. Youβll need to specify that requirement so that other developers and automated systems can run your application.
So you decide to include the flask dependency in a requirements.txt file:
flask
Great, everything works fine locally, and after hacking away on your app for a while, you decide to move it to production. Hereβs where things get a little scaryβ¦
The above requirements.txt file doesnβt specify which version of flask to use. In this case, pip install -r requirements.txt will install the latest version by default. This is okay unless there are interface or behavior changes in the newest version that break our application.
For the sake of this example, letβs say that a new version of flask got released. However, it isnβt backward compatible with the version you used during development.
Now, letβs say you deploy your application to production and do a pip install -r requirements.txt. Pip gets the latest, not-backward-compatible version of flask, and just like that, your application breaksβ¦ in production.
βBut hey, it worked on my machine!ββIβve been there myself, and itβs not a great feeling.
At this point, you know that the version of flask you used during development worked fine. So, to fix things, you try to be a little more specific in your requirements.txt. You add a version specifier to the flask dependency. This is also called pinning a dependency:
flask==0.12.1
Pinning the flask dependency to a specific version ensures that a pip install -r requirements.txt sets up the exact version of flask you used during development. But does it really?
Keep in mind that flask itself has dependencies as well (which pip installs automatically). However, flask itself doesnβt specify exact versions for its dependencies. For example, it allows any version of Werkzeug>=0.14.
Again, for the sake of this example, letβs say a new version of Werkzeug got released, but it introduces a show-stopper bug to your application.
When you do pip install -r requirements.txt in production this time, you will get flask==0.12.1 since youβve pinned that requirement. However, unfortunately, youβll get the latest, buggy version of Werkzeug. Again, the product breaks in production.
The real issue here is that the build isnβt deterministic. What I mean by that is that, given the same input (the requirements.txt file), pip doesnβt always produce the same environment. At the moment, you canβt easily replicate the exact environment you have on your development machine in production.
The typical solution to this problem is to use pip freeze. This command allows you to get exact versions for all 3rd party libraries currently installed, including the sub-dependencies pip installed automatically. So you can freeze everything in development to ensure that you have the same environment in production.
Executing pip freeze results in pinned dependencies you can add to a requirements.txt:
click==6.7
Flask==0.12.1
itsdangerous==0.24
Jinja2==2.10
MarkupSafe==1.0
Werkzeug==0.14.1
With these pinned dependencies, you can ensure that the packages installed in your production environment match those in your development environment exactly, so your product doesnβt unexpectedly break. This βsolution,β unfortunately, leads to a whole new set of problems.
Now that youβve specified the exact versions of every third-party package, you are responsible for keeping these versions up to date, even though theyβre sub-dependencies of flask. What if thereβs a security hole discovered in Werkzeug==0.14.1 that the package maintainers immediately patched in Werkzeug==0.14.2? You really need to update to Werkzeug==0.14.2 to avoid any security issues arising from the earlier, unpatched version of Werkzeug.
First, you need to be aware that thereβs an issue with the version you have. Then, you need to get the new version in your production environment before someone exploits the security hole. So, you have to change your requirements.txt manually to specify the new version Werkzeug==0.14.2. As you can see in this situation, the responsibility of staying up to date with necessary updates falls on you.
The truth is that you really donβt care what version of Werkzeug gets installed as long as it doesnβt break your code. In fact, you probably want the latest version to ensure that youβre getting bug fixes, security patches, new features, more optimization, and so on.
The real question is: βHow do you allow for deterministic builds for your Python project without gaining the responsibility of updating versions of sub-dependencies?β
Spoiler alert: The easy answer is using Pipenv.
Development of Projects with Different Dependencies
Letβs switch gears a bit to talk about another common issue that arises when youβre working on multiple projects. Imagine that ProjectA needs django==1.9, but ProjectB needs django==1.10.
By default, Python tries to store all your third-party packages in a system-wide location. This means that every time you want to switch between ProjectA and ProjectB, you have to make sure the right version of django is installed. This makes switching between projects painful because you have to uninstall and reinstall packages to meet the requirements for each project.
The standard solution is to use a virtual environment that has its own Python executable and third-party package storage. That way, ProjectA and ProjectB are adequately separated. Now you can easily switch between projects since theyβre not sharing the same package storage location. PackageA can have whatever version of django it needs in its own environment, and PackageB can have what it needs totally separate. A very common tool for this is virtualenv (or venv in Python 3).
Pipenv has virtual environment management built in so that you have a single tool for your package management.
Dependency Resolution
What do I mean by dependency resolution? Letβs say youβve got a requirements.txt file that looks something like this:
package_a
package_b
Letβs say package_a has a sub-dependency package_c, and package_a requires a specific version of this package: package_c>=1.0. In turn, package_b has the same sub-dependency but needs package_c<=2.0.
Ideally, when you try to install package_a and package_b, the installation tool would look at the requirements for package_c (being >=1.0 and <=2.0) and select a version that fulfills those requirements. Youβd hope that the tool resolves the dependencies so that your program works in the end. This is what I mean by βdependency resolution.β
Unfortunately, pip itself doesnβt have real dependency resolution at the moment, but thereβs an open issue to support it.
The way pip would handle the above scenario is as follows:
-
It installs
package_aand looks for a version ofpackage_cthat fulfills the first requirement (package_c>=1.0). -
Pip then installs the latest version of
package_cto fulfill that requirement. Letβs say the latest version ofpackage_cis 3.1.
This is where the trouble (potentially) starts.
If the version of package_c selected by pip doesnβt fit future requirements (such as package_b needing package_c<=2.0), the installation will fail.
The βsolutionβ to this problem is to specify the range required for the sub-dependency (package_c) in the requirements.txt file. That way, pip can resolve this conflict and install a package that meets those requirements:
package_c>=1.0,<=2.0
package_a
package_b
Just like before though, youβre now concerning yourself directly with sub-dependencies (package_c). The issue with this is that if package_a changes their requirement without you knowing, the requirements you specified (package_c>=1.0,<=2.0) may no longer be acceptable, and installation may failβ¦ again. The real problem is that once again, youβre responsible for staying up to date with requirements of sub-dependencies.
Ideally, your installation tool would be smart enough to install packages that meet all the requirements without you explicitly specifying sub-dependency versions.
Pipenv Introduction
Now that weβve addressed the problems, letβs see how Pipenv solves them.
First, letβs install it:
$ pip install pipenv
Once youβve done that, you can effectively forget about pip since Pipenv essentially acts as a replacement. It also introduces two new files, the Pipfile (which is meant to replace requirements.txt) and the Pipfile.lock (which enables deterministic builds).
Pipenv uses pip and virtualenv under the hood but simplifies their usage with a single command line interface.
Example Usage
Letβs start over with creating your awesome Python application. First, spawn a shell in a virtual environment to isolate the development of this app:
$ pipenv shell
This will create a virtual environment if one doesnβt already exist. Pipenv creates all your virtual environments in a default location. If you want to change Pipenvβs default behavior, there are some environmental variables for configuration.
You can force the creation of a Python 2 or 3 environment with the arguments --two and --three respectively. Otherwise, Pipenv will use whatever default virtualenv finds.
Sidenote: If you require a more specific version of Python, you can provide a
--pythonargument with the version you require. For example:--python 3.6
Now you can install the 3rd party package you need, flask. Oh, but you know that you need version 0.12.1 and not the latest version, so go ahead and be specific:
$ pipenv install flask==0.12.1
You should see something like the following in your terminal:
Adding flask==0.12.1 to Pipfile's [packages]...
Pipfile.lock not found, creating...
Youβll notice that two files get created, a Pipfile and Pipfile.lock. Weβll take a closer look at these in a second. Letβs install another 3rd party package, numpy, for some number-crunching. You donβt need a specific version so donβt specify one:
$ pipenv install numpy
If you want to install something directly from a version control system (VCS), you can! You specify the locations similarly to how youβd do so with pip. For example, to install the requests library from version control, do the following:
$ pipenv install -e git+https://github.com/psf/requests.git@main#egg=requests
Note the -e argument above to make the installation editable. Currently, this is required for Pipenv to do sub-dependency resolution.
Letβs say you also have some unit tests for this awesome application, and you want to use pytest for running them. You donβt need pytest in production so you can specify that this dependency is only for development with the --dev argument:
$ pipenv install pytest --dev
Providing the --dev argument will put the dependency in a special [dev-packages] location in the Pipfile. These development packages only get installed if you specify the --dev argument with pipenv install.
The different sections separate dependencies needed only for development from ones needed for the base code to actually work. Typically, this would be accomplished with additional requirements files like dev-requirements.txt or test-requirements.txt. Now, everything is consolidated in a single Pipfile under different sections.
Okay, so letβs say youβve got everything working in your local development environment and youβre ready to push it to production. To do that, you need to lock your environment so you can ensure you have the same one in production:
$ pipenv lock
This will create/update your Pipfile.lock, which youβll never need to (and are never meant to) edit manually. You should always use the generated file.
Now, once you get your code and Pipfile.lock in your production environment, you should install the last successful environment recorded:
$ pipenv install --ignore-pipfile
This tells Pipenv to ignore the Pipfile for installation and use whatβs in the Pipfile.lock. Given this Pipfile.lock, Pipenv will create the exact same environment you had when you ran pipenv lock, sub-dependencies and all.
The lock file enables deterministic builds by taking a snapshot of all the versions of packages in an environment (similar to the result of a pip freeze).
Now letβs say another developer wants to make some additions to your code. In this situation, they would get the code, including the Pipfile, and use this command:
$ pipenv install --dev
This installs all the dependencies needed for development, which includes both the regular dependencies and those you specified with the --dev argument during install.
When an exact version isnβt specified in the Pipfile, the
installcommand gives the opportunity for dependencies (and sub-dependencies) to update their versions.
This is an important note because it solves some of the previous problems we discussed. To demonstrate, letβs say a new version of one of your dependencies becomes available. Because you donβt need a specific version of this dependency, you donβt specify an exact version in the Pipfile. When you pipenv install, the new version of the dependency will be installed in your development environment.
Now you make your changes to the code and run some tests to verify everything is still working as expected. (You do have unit tests, right?) Now, just as before, you lock your environment with pipenv lock, and an updated Pipfile.lock will be generated with the new version of the dependency. Just as before, you can replicate this new environment in production with the lock file.
As you can see from this scenario, you no longer have to force exact versions you donβt truly need to ensure your development and production environments are the same. You also donβt need to stay on top of updating sub-dependencies you βdonβt care about.β This workflow with Pipenv, combined with your excellent testing, fixes the issues of manually doing all your dependency management.
Pipenvβs Dependency Resolution Approach
Pipenv will attempt to install sub-dependencies that satisfy all the requirements from your core dependencies. However, if there are conflicting dependencies (package_a needs package_c>=1.0, but package_b needs package_c<1.0), Pipenv will not be able to create a lock file and wil output an error like the following:
Warning: Your dependencies could not be resolved. You likely have a mismatch in your sub-dependencies.
You can use $ pipenv install --skip-lock to bypass this mechanism, then run $ pipenv graph to inspect the situation.
Could not find a version that matches package_c>=1.0,package_c<1.0
As the warning says, you can also show a dependency graph to understand your top-level dependencies and their sub-dependencies:
$ pipenv graph
This command will print out a tree-like structure showing your dependencies. Hereβs an example:
Flask==0.12.1
- click [required: >=2.0, installed: 6.7]
- itsdangerous [required: >=0.21, installed: 0.24]
- Jinja2 [required: >=2.4, installed: 2.10]
- MarkupSafe [required: >=0.23, installed: 1.0]
- Werkzeug [required: >=0.7, installed: 0.14.1]
numpy==1.14.1
pytest==3.4.1
- attrs [required: >=17.2.0, installed: 17.4.0]
- funcsigs [required: Any, installed: 1.0.2]
- pluggy [required: <0.7,>=0.5, installed: 0.6.0]
- py [required: >=1.5.0, installed: 1.5.2]
- setuptools [required: Any, installed: 38.5.1]
- six [required: >=1.10.0, installed: 1.11.0]
requests==2.18.4
- certifi [required: >=2017.4.17, installed: 2018.1.18]
- chardet [required: >=3.0.2,<3.1.0, installed: 3.0.4]
- idna [required: >=2.5,<2.7, installed: 2.6]
- urllib3 [required: <1.23,>=1.21.1, installed: 1.22]
From the output of pipenv graph, you can see the top-level dependencies we installed previously (Flask, numpy, pytest, and requests), and underneath you can see the packages they depend on.
Additionally, you can reverse the tree to show the sub-dependencies with the parent that requires it:
$ pipenv graph --reverse
This reversed tree may be more useful when you are trying to figure out conflicting sub-dependencies.
The Pipfile
Pipfile intends to replace requirements.txt. Pipenv is currently the reference implementation for using Pipfile. It seems very likely that pip itself will be able to handle these files. Also, itβs worth noting that Pipenv is even the official package management tool recommended by Python itself.
The syntax for the Pipfile is TOML, and the file is separated into sections. [dev-packages] for development-only packages, [packages] for minimally required packages, and [requires] for other requirements like a specific version of Python. See an example file below:
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[dev-packages]
pytest = "*"
[packages]
flask = "==0.12.1"
numpy = "*"
requests = {git = "https://github.com/requests/requests.git", editable = true}
[requires]
python_version = "3.6"
Ideally, you shouldnβt have any sub-dependencies in your Pipfile. What I mean by that is you should only include the packages you actually import and use. No need to keep chardet in your Pipfile just because itβs a sub-dependency of requests. (Pipenv will install it automatically.) The Pipfile should convey the top-level dependencies your package requires.
The Pipfile.lock
This file enables deterministic builds by specifying the exact requirements for reproducing an environment. It contains exact versions for packages and hashes to support more secure verification, which pip itself now supports as well. An example file might look like the following. Note that the syntax for this file is JSON and that Iβve excluded parts of the file with ...:
{
"_meta": {
...
},
"default": {
"flask": {
"hashes": [
"sha256:6c3130c8927109a08225993e4e503de4ac4f2678678ae211b33b519c622a7242",
"sha256:9dce4b6bfbb5b062181d3f7da8f727ff70c1156cbb4024351eafd426deb5fb88"
],
"version": "==0.12.1"
},
"requests": {
"editable": true,
"git": "https://github.com/requests/requests.git",
"ref": "4ea09e49f7d518d365e7c6f7ff6ed9ca70d6ec2e"
},
"werkzeug": {
"hashes": [
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b",
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c"
],
"version": "==0.14.1"
}
...
},
"develop": {
"pytest": {
"hashes": [
"sha256:8970e25181e15ab14ae895599a0a0e0ade7d1f1c4c8ca1072ce16f25526a184d",
"sha256:9ddcb879c8cc859d2540204b5399011f842e5e8823674bf429f70ada281b3cc6"
],
"version": "==3.4.1"
},
...
}
}
Note the exact version specified for every dependency. Even the sub-dependencies like werkzeug that arenβt in our Pipfile appear in this Pipfile.lock. The hashes are used to ensure youβre retrieving the same package as you did in development.
Itβs worth noting again that you should never change this file by hand. It is meant to be generated with pipenv lock.
Pipenv Extra Features
Open a third-party package in your default editor with the following command:
$ pipenv open flask
This will open the flask package in the default editor, or you can specify a program with an EDITOR environmental variable. For example, I use Sublime Text, so I just set EDITOR=subl. This makes it super simple to dig into the internals of a package youβre using.
You can run a command in the virtual environment without launching a shell:
$ pipenv run <insert command here>
Check for security vulnerabilities (and PEP 508 requirements) in your environment:
$ pipenv check
Now, letβs say you no longer need a package. You can uninstall it:
$ pipenv uninstall numpy
Additionally, letβs say you want to completely wipe all the installed packages from your virtual environment:
$ pipenv uninstall --all
You can replace --all with --all-dev to just remove dev packages.
Pipenv supports the automatic loading of environmental variables when a .env file exists in the top-level directory. That way, when you pipenv shell to open the virtual environment, it loads your environmental variables from the file. The .env file just contains key-value pairs:
SOME_ENV_CONFIG=some_value
SOME_OTHER_ENV_CONFIG=some_other_value
Finally, here are some quick commands to find out where stuff is. How to find out where your virtual environment is:
$ pipenv --venv
How to find out where your project home is:
$ pipenv --where
Package Distribution
You may be asking how this all works if you intend to distribute your code as a package.
Yes, I need to distribute my code as a package
How does Pipenv work with setup.py files?
There are a lot of nuances to that question. First, a setup.py file is necessary when youβre using setuptools as your build/distribution system. This has been the de facto standard for a while now, but recent changes have made the use of setuptools optional.
This means that projects like flit can use the new pyproject.toml to specify a different build system that doesnβt require a setup.py.
All that being said, for the near future setuptools and an accompanying setup.py will still be the default choice for many people.
Hereβs a recommended workflow for when you are using a setup.py as a way to distribute your package:
setup.pyinstall_requireskeyword should include whatever the package βminimally needs to run correctly.βPipfile- Represents the concrete requirements for your package
- Pull the minimally required dependencies from
setup.pyby installing your package using Pipenv:- Use
pipenv install '-e .' - That will result in a line in your
Pipfilethat looks something like"e1839a8" = {path = ".", editable = true}.
- Use
Pipfile.lock- Details for a reproducible environment generated from
pipenv lock
To clarify, put your minimum requirements in setup.py instead of directly with pipenv install. Then use the pipenv install '-e .' command to install your package as editable. This gets all the requirements from setup.py into your environment. Then you can use pipenv lock to get a reproducible environment.
I donβt need to distribute my code as a package
Great! If you are developing an application that isnβt meant to be distributed or installed (a personal website, a desktop application, a game, or similar), you donβt really need a setup.py.
In this situation, you could use Pipfile/Pipfile.lock combo for managing your dependencies with the flow described previously to deploy a reproducible environment in production.
I already have a requirements.txt. How do I convert to a Pipfile?
If you run pipenv install it should automatically detect the requirements.txt and convert it to a Pipfile, outputting something like the following:
requirements.txt found, instead of Pipfile! Convertingβ¦
Warning: Your Pipfile now contains pinned versions, if your requirements.txt did.
We recommend updating your Pipfile to specify the "*" version, instead.
Take note of the above warning.
If you have pinned exact versions in your requirements.txt file, youβll probably want to change your Pipfile to only specify exact versions you truly require. This will allow you to gain the real benefits of transitioning. For example, letβs say you have the following but really donβt need that exact version of numpy:
[packages]
numpy = "==1.14.1"
If you donβt have any specific version requirements for your dependencies, you can use the wildcard character * to tell Pipenv that any version can be installed:
[packages]
numpy = "*"
If you feel nervous about allowing any version with the *, itβs typically a safe bet to specify greater than or equal to the version youβre already on so you can still take advantage of new versions:
[packages]
numpy = ">=1.14.1"
Of course, staying up to date with new releases also means youβre responsible for ensuring your code still functions as expected when packages change. This means a test suite is essential to this whole Pipenv flow if you want to ensure functioning releases of your code.
You allow packages to update, run your tests, ensure they all pass, lock your environment, and then you can rest easy knowing that you havenβt introduced breaking changes. If things do break because of a dependency, youβve got some regression tests to write and potentially some more restrictions on versions of dependencies.
For example, if numpy==1.15 gets installed after running pipenv install and it breaks your code, which you hopefully either notice during development or during your tests, you have a couple options:
-
Update your code to function with the new version of the dependency.
If backward compatibility with previous versions of the dependency isnβt possible, youβll also need to bump your required version in your
Pipfile:Config File[packages] numpy = ">=1.15" -
Restrict the version of the dependency in the
Pipfileto be<the version that just broke your code:Config File[packages] numpy = ">=1.14.1,<1.15"
Option 1 is preferred as it ensures that your code is using the most up-to-date dependencies. However, Option 2 takes less time and doesnβt require code changes, just restrictions on dependencies.
You can also install from requirement files with the same -r argument pip takes:
$ pipenv install -r requirements.txt
If you have a dev-requirements.txt or something similar, you can add those to the Pipfile as well. Just add the --dev argument so it gets put in the right section:
$ pipenv install -r dev-requirements.txt --dev
Additionally, you can go the other way and generate requirements files from a Pipfile:
$ pipenv lock -r > requirements.txt
$ pipenv lock -r -d > dev-requirements.txt
Whatβs next?
It appears to me that a natural progression for the Python ecosystem would be a build system that uses the Pipfile to install the minimally required dependencies when retrieving and building a package from a package index (like PyPI). It is important to note again that the Pipfile design specification is still in development, and Pipenv is just a reference implementation.
That being said, I could see a future where the install_requires section of setup.py doesnβt exist, and the Pipfile is referenced for minimal requirements instead. Or the setup.py is gone entirely, and you get metadata and other information in a different manner, still using the Pipfile to get the necessary dependencies.
Is Pipenv worth checking out?
Definitely. Even if itβs just as a way to consolidate the tools you already use (pip & virtualenv) into a single interface. However, itβs much more than that. With the addition of the Pipfile, you only specify the dependencies you truly need.
You no longer have the headache of managing the versions of everything yourself just to ensure you can replicate your development environment. With the Pipfile.lock, you can develop with peace of mind knowing that you can exactly reproduce your environment anywhere.
In addition to all that, it seems very likely that the Pipfile format will get adopted and supported by official Python tools like pip, so itβd be beneficial to be ahead of the game. Oh, and make sure youβre updating all your code to Python 3 as well: 2020 is coming up fast.
References, further reading, interesting discussions, and so forth
- Official Pipenv documentation
- Official
PipfileProject - Issue addressing
install_requiresin regards toPipfile - More discussion on
setup.pyvsPipfile - Post talking about PEP 518
- Post on Python packaging
- Comment suggesting Pipenv usage
Free Bonus: Click here to get access to a free 5-day class that shows you how to avoid common dependency management issues with tools like Pip, PyPI, Virtualenv, and requirements files.
Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: Working With Pipenv

