Dependency management is a critical part of project development. If it were done wrong, project would behave differently between development and production environments. With Python, we have the tool virtualenv
that isolates the project’s environment from the system’s, and we use pip
and a requirement.txt
file to maintain the list of dependencies. For instance:
1 | Flask==3.0.0 |
And the environment can be setup by:
1 | % python3 -m venv venv |
Disadvantages of the requirements.txt
approach
There are several shortcomings of this method. First and the major problem is this file only contains the direct dependencies, not the transitive ones. pip freeze
shows that Flask and Flask-SQLAlchemy depend on several other packages:
1 | % pip freeze |
Take Werkzeug for an example. It is required by Flask, and its version is stated as Werkzeug>=3.0.0
in Flask’s project file. This may cause a problem when Werkzeug bumps its version to 4.x and after a reinstallation of the project, it will download the latest version of Werkzeug and create a compatibility issue. Same thing may happen to Flask-SQLAlchemy since functions of SQLAlchemy may vary between major versions.
A possible solution is to freeze the dependencies altogether:
1 | % pip freeze >requirements.txt |
This is practically the lockfile we see in other languages like yarn.lock
and Gemfile.lock
, whereby the installation process is fully reproducible. But for Python, we need extra effort to ensure that the requirements.txt
is updated correctly after modifying the dependencies. And it also makes it difficult to upgrade the direct dependencies because the transitive ones need to be upgraded manually.
Another problem of requirements.txt
is when we need to maintain different dependencies across environments. For instance, in development we may want to include ruff
and mypy
, and in production, gunicorn
is required. In practice, we create two separate files for this purpose, the requirements.txt
:
1 | Flask==3.0.0 |
And requirements-dev.txt
:
1 | ruff==0.1.11 |
Then add a Makefile
to facilitate the installation:
1 | install: |
Lock dependency versions with poetry.lock
Though requirements.txt
has some problems, we have been using this approach in production for years. And thanks to the opensource community, we now have a better option, Poetry. I would like to call it Yarn for Python, because I maintain several frontend projects and always hope there would be a tool that is as effective and easy-to-use as Yarn. With Poetry, we can simply use poetry install
to lock dependency versions. And it does much more than that.
There are several ways to install Poetry. One can follow the instructions on its official documentation. For Homebrew users, simply use brew install poetry
. Later in this article I will discuss how to use Poetry when deploying application with Docker. Here are some frequently used commands:
poetry init
starts an interactive tool to initialize an existing project. When finished, it will generate apyproject.toml
file at the root of the project.poetry install
creates a virtual environment, installs dependencies according to the specification inpyproject.toml
, and writes thepoetry.lock
file. Make sure you commit this file to the repository, so that other people can install the exact same versions when they clone your repository and runpoetry install
.poetry add flask
adds the latest version of Flask to project’s dependencies. The preferable way to specify version is using the caret symbol with Semantic Versioning. For example^3.0.0
means the version ranges from3.0.0
(inclusive) to4.0.0
(exclusive).poetry lock
should be used when you have manually edited thepyproject.toml
file, so thatpoetry.lock
can be updated accordingly.poetry run flask run
starts the Flask development server within the virtualenv.
Where is the virtual environment?
Unlike the traditional approach, Poetry does not create a venv
folder in project’s root. Instead, it collects all virtual environments in one place, for different projects and different Python versions.
1 | % ls $(poetry config virtualenvs.path) |
When you have python3.11
and python3.12
in the PATH
, you can use poetry env use 3.12
to switch versions. Poetry also works with pyenv, or even custom-built Python binary.
If you prefer creating the virtual environment under the project’s directory, set virtualenvs.in-project
to true
. Configurations are by default set globally. Add --local
to set it locally in the project. Another option is to create virtualenv on your own. Make sure the folder is named .venv
and Poetry will automatically pick it up.
1 | % poetry env remove --all |
We can also install dependencies into the system’s environment. We will look into that in the Docker section.
Manage dependencies for different environments
As mentioned above, we need different dependencies for development and production environment. Specifically, we want Flask
, ruff
, mypy
to be installed in development environment, and Flask
, gunicorn
to be installed in production. To achieve that, we put ruff
and mypy
in a dependency group named dev
, and specify gunicorn
as a package extra. Here’s what we do in the pyproject.toml
:
1 | [tool.poetry.dependencies] |
In development environment, we simply use poetry install
. In production, use the following command:
1 | poetry install --without dev --extras gunicorn |
Deploy application with Poetry and Docker
1 | FROM python:3.11-slim |
- It is advised to install Poetry in a dedicated virtual environment, so we create one and install Poetry via
pip
. - Since the container only has one application, it is safe to install project’s dependencies into the system environment. Simply set
virtualenvs.create
tofalse
. Or you can let Poetry create a dedicated one for the project. - Project code resides in
/app
, and it is set as the working directory. So whengunicorn
is called, which is available at system level, it can findpoetrydemo
package without problem.
1 | % docker build -t poetrydemo . |
Use a PyPI mirror
Last but not least, if you are in an area where internet access is restricted, the usual mirror config in ~/.pip/pip.conf
does not work, since Poetry only processes its own config files. To use a mirror for PyPI, add the following config into pyproject.toml
:
1 | [[tool.poetry.source]] |