Pipenv is the official recommendation for dependency management and virtual environment management in Python. It aims to bring both into a single tool, simplifying the workflow.
Here’s Pipenv managing dependencies for a simple Flask app.
First, let’s create a project directory and initialize Pipenv:
mkdir my_flask_app
cd my_flask_app
pipenv --python 3.9
This creates a Pipfile and a Pipfile.lock in your project directory. The Pipfile is where you’ll declare your direct dependencies, and Pipfile.lock will track the exact versions of all installed packages, including transitive dependencies.
Now, let’s install Flask:
pipenv install flask
Pipenv will create a virtual environment (if one doesn’t exist for this project) and install Flask into it. It will also update your Pipfile and Pipfile.lock.
Your Pipfile will now look something like this:
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
flask = "*"
[dev-packages]
[requires]
python_version = "3.9"
Notice flask = "*". This means "install the latest compatible version of Flask". The Pipfile.lock will contain the exact version, e.g., Flask==2.2.2.
To run your Python script within the virtual environment, you use pipenv run:
echo "from flask import Flask; app = Flask(__name__); @app.route('/')\ndef hello(): return 'Hello, World!'\nif __name__ == '__main__': app.run(debug=True)" > app.py
pipenv run python app.py
This starts the Flask development server.
To activate the virtual environment in your shell, so you don’t have to prefix commands with pipenv run, use pipenv shell:
pipenv shell
Your shell prompt will change, indicating you’re inside the virtual environment. Now, you can run commands directly:
(my_flask_app) $ python app.py
To install development dependencies (like testing tools or linters), you use the --dev flag:
pipenv install pytest --dev
This adds pytest to the [dev-packages] section of your Pipfile.
Pipenv automatically manages dependencies by default. When you run pipenv install <package>, it resolves the latest compatible version according to the constraints in your Pipfile, installs it, and then records the exact version in Pipfile.lock. This ensures reproducible builds across different environments. If you run pipenv install without any arguments, it will install all dependencies specified in Pipfile.lock.
The Pipfile.lock is the key to reproducible environments. It specifies the exact versions of all packages, including transitive dependencies. When you share your project, another developer can run pipenv install and get the exact same set of packages installed, preventing "it works on my machine" issues.
When you update a package, say pipenv update flask, Pipenv will find the latest compatible version of Flask and its dependencies, update Pipfile.lock accordingly, and install the new versions. If you want to pin a package to a specific version, you can edit the Pipfile directly, e.g., flask = "==2.2.2", and then run pipenv install to update the lock file.
The surprising truth is that Pipenv’s virtual environment management is a side effect of its dependency locking mechanism. It creates a dedicated virtual environment for each project because it needs an isolated place to install the exact versions specified in the Pipfile.lock without interfering with other projects or the system Python. This isolation is crucial for ensuring that the precise dependency graph is maintained.
The next logical step is understanding how to manage different environments (e.g., development, staging, production) with Pipenv, often involving Pipfile markers or separate lock files.