Getting Started With Requirements Files
You have probably had the experience before of receiving a piece of Python code from someone, and trying to run the code, only to receive a ModuleNotFoundError
or an AttributeError
stating that a particular module or attribute does not exist. This can be a frustrating experience, as you work your way through the code, gradually finding the required libraries and versions that are needed to get the code to run correctly. Fortunately, there is a simple way to avoid this issue, using a requirements file in every project and script you create.
In this article, we'll cover the basics of what requirements files are, the basic syntax behind them, and why the principle of managing dependencies should be a part of every Python project you work on. Though there are different techniques for managing dependencies with your projects and scripts, using a requirements file is a straightforward and simple approach to making your code more stable and portable.
Table of Contents
- What are requirements files?
- Requirements File Syntax
- Specifying Versions
- Managing Dependencies
- A Note for the Future
- Further Practice and Reading
What are requirements files?
Requirements files are lists of packages that are installed by pip when running the pip install
command and specifying the file name. These files are used not only by pip, but are also forms of documentation for your project, helping other developers get a sense of what your project might be doing at a high level.
A common convention with your requirements file is to name it requirements.txt
(or some variation of that), which you will often see in open source projects. For example, here is the requirements-dev.txt
file from the Requests library.
Once you have created a requirements file, it is very easy to install all of the requirements using the pip install
command. Let's create a very basic requirements file below and install it.
$ echo "pandas" >> requirements.txt
$ pip install -r requirements.txt
And that's it, pip installs everything you need and you are ready to go with your project.
Note: It is considered best practice to use a virtual environment (via venv, virtualenv, or a tool like conda) when installing packages for a project. This helps keep your dependencies isolated and avoids conflicts.
Requirements File Syntax
A requirements file is typically a text file with a series of one or more lines, each containing a specific library that should be installed for your project. A basic example is shown below.
# Sample requirements.txt file
requests
numpy
pandas
scikit-learn
mlflow
You'll see from the example above that you can include comments and blank lines in the file, this can be a helpful way to create logical groupings of requirements or to specify why a particular version was chosen. The full syntax specification of the file can be found here. There are several variations and advanced features for requirements files that can be put in place, but in this article, we'll stick to the basics.
Specifying Versions
Now that we've covered the basics of creating a requirements file and identifying which packages are a part of your requirements, let's take a look at some of the different ways you can specify acceptable versions of listed packages. We'll use our same list of requirements from the previous example and show how you can "pin" specific versions of a package with specifiers.
# Sample requirements.txt file
requests==2.32.5
numpy>=2.0.0
pandas!=1.5.2
scikit-learn
mlflow~=3.0
The Python packaging documentation has specifics on the different syntax that is possible when specifying a version, let's go over them briefly here. We'll start with the most commonly used specifiers and work down to less common syntax.
==
: This is the version matching clause, which is a strict equality comparison. So if you specifyrequests==2.32.5
in your requirements file, that is the version that pip will install, which can make your script more reproducible.<=
/>=
&<
/>
: These are ordered comparison clauses, both inclusive (<=
/>=
) and exclusive (<
/>
). You might often see these ordered clauses used in the context of specifying a package "after" or "before" a given version, perhaps for a security reason or for a compatibility reason.!=
: This is the version exclusion clause, basically, you are saying that you will accept any other version of the package except for this specific version.~=
: This is the compatible release clause, which matches any version of a package which is expected to be compatible with the version of the package you specify. This can be used in an example where you are ok with any version of the package that is in the same major release number, such as any3.x.x
version. In this example, that could be specified as~=3.0
. At the time of this writing (August 2025), this specifier matches mlflow version 3.3.2- Note that this should be used with caution, as not all libraries strictly follow semantic versioning principles and there may be incompatibility issues that arise outside of major version releases. Here is a small table that summarizes the common version specifiers covered here
Specifier | Meaning | Example |
---|---|---|
== |
Exactly this version | pandas==2.2.2 |
>= |
This version or newer | numpy>=1.25.0 |
<= |
This version or older | scipy<=1.11.2 |
> |
Newer than this version | numpy>1.25.0 |
< |
Older than this version | scipy<1.11.2 |
!= |
Any version except this one | pydantic!=2.3.0 |
~= |
Compatible with this version | mlflow~=3.0 |
Managing Dependencies
Having covered the basics of requirements files, their syntax, and specifying versions, you may be thinking that this seems like a lot of work or wondering if there are ways to shortcut this, perhaps through some functionality within pip. It turns out that there is (kind of), but one that comes with some caveats.
Usage of pip freeze
Remember the five packages that we specified before? If you created your own requirements file and installed it using pip, you may have noticed that there were a sizable number of other package dependencies that were installed as well. Running the command pip freeze
will show you the full list of packages that are installed in your current environment, along with the versions, in a format similar to our requirements file
$ pip freeze
> alembic==1.16.5
> annotated-types==0.7.0
> anyio==4.10.0
> blinker==1.9.0
> cachetools==5.5.2
> certifi==2025.8.3
# Omitted for brevity...
> requests==2.32.5
> threadpoolctl==3.6.0
> typing-inspection==0.4.1
> typing_extensions==4.15.0
> tzdata==2025.2
> urllib3==2.5.0
> uvicorn==0.35.0
> Werkzeug==3.1.3
> zipp==3.23.0
You might see this as a shortcut to specifying the dependencies in your requirements file, just copy and paste the output in and you're done! But this approach can come with some significant drawbacks that you need to consider before you choose to proceed, some of which I'll go over here.
- If you over-specify your requirements, you won't get bug fixes, security updates, or performance improvements. Using the example I showed earlier (
requests==2.32.5
), what happens if a vulnerability is discovered in the requests library and a patch release is deployed to fix it? If you've specified all the way down to the patch version, you will miss this vulnerability patching. - You may unintentionally miss out on compatible upgrades that don’t break your code. This could happen if you specify a minor release version for a project. If this project follows semantic versioning principles, new functionality released in a minor version will by definition be backwards-compatible, so you would miss out on this.
- If you use
pip freeze
, you may end up with developer tools that shouldn’t be in production. As an example, maybe you're using pytest and ruff to run your unit tests and lint your code. These are likely not needed in production and shouldn't be included. - If you are working on a cross-platform team, you need to exercise caution as platform-specific builds or binary incompatibilities could be introduced in an overly-specific requirements file.
- As software evolves, it is natural that older versions of libraries may become broken, no longer installing cleanly. If your requirements file does not have enough flexibility to handle this, you may break your project.
These examples illustrate tradeoffs that you will need to consider when deciding how flexible or specific you should be for your requirements.
This technique of using
pip freeze
may be useful in specific, limited contexts, such as deploying a version of your project with Docker, where you have full control over the entire system your project will run on. Another very important scenario where usingpip freeze
may be warranted is in the context of reproducing research, where being able to run the exact version of the code and libraries used to produce and analyze your data is extremely important.
So, What to Do?
If we feel that using pip freeze
has drawbacks in our specific project or script, how do we decide what we need to specify? The guidance that I give newer developers is to only include the packages you explicitly import in your code, and specify version constraints only when necessary. If you're writing a script that uses pandas to do some data manipulation, it's okay to only have the pandas library listed in your requirements file. If you're only using basic functionality of the library, then it's ok to only specify the major version number using one of the version specification techniques we discussed above. This gives you flexibility and simplifies the work that a future developer needs to do to pick up and work with your code.
A Note for the Future
Many newer projects are adopting the usage of a pyproject.toml
file to manage their project dependencies, along with other build metadata. The pyproject.toml
file is part of a newer standard for specifying packaging and build metadata for Python projects, and is supported by popular tools like Poetry or uv. While this is an excellent approach for your projects, I believe that you should use a requirements file as your minimum acceptable baseline for any project you are working on. If you would like to include more metadata on your project, then using a pyproject.toml
file can be an excellent strategy. Here's a sample pyproject.toml
file specifying dependencies.
[project]
dependencies = [
"requests==2.32.5",
"numpy>=2.0.0",
"pandas!=1.5.2",
"scikit-learn",
"mlflow~=3.0"
]
Whichever method you choose for specifying dependencies, future developers and maintainers will be thankful that you have explicitly documented your development system and runtime expectations.
Further Practice and Reading
Now that we've gone through the basics of requirements files and how they can help you improve your projects, I would encourage you to read through the sources that are listed below. These will help you get a more thorough understanding of how requirements files work and the nuances of different formatting options that will help you more effectively convey your project to other developers and future maintainers.
As a final takeaway, consider the following guidelines for using requirements files.
- Use virtual environments to isolate project dependencies
- Only list imported packages in your requirements file
- Pin specific versions carefully, and only when needed
- Don't use
pip freeze
carelessly, ensure each requirement serves a purpose - Consider
pyproject.toml
files if your project grows in complexity and maturity Following these guidelines will help you write code that is easier to share, collaborate on, and deploy, whether you work on personal projects or production applications.