Python Dev Environment Part 3: dependencies with install_requires and requirements.txt
Tags: python
Published: 2020-07-23 10:49:10

In [Part 1] of this series, we created a simple Python distribution package called "pygreet." In [Part 2], we wrote tests and ran them using pytest.

Other than pytest, we did not install anything that wasn't already included in the Python standard library. Often, though, a package will have several dependencies. How do we best handle these?

Simply put, place package dependencies in the `install_requires` list in `setup.py`. Place _development_ dependencies, such as test frameworks and linters, in `requirements.txt`.

Let's explore further.

## Add a function with a dependency

Take the pygreet package we have been building and add a friendly time-telling function.

```python
"""Functions useful for sending greetings."""

import arrow

def greet(greeting="Hello", recipient="World"):
    """Greet someone."""
    return f"{greeting}, {recipient}"

def greet_city(tz):
    """Greet a location."""
    now = arrow.now(tz)
    friendly_time = now.format("h:mm a")
    location = tz.split("/")[-1].replace("_"," ") 
    greeting = greet("Hello", location)
    greeting += f"! The time is {friendly_time}."
    return greeting
```

We have added two things: `import arrow` and the `greet_city` function, which takes a [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) as input.

## Include list of dependencies in `install_requires` in `setup.py`

Because arrow is an external dependency, let's add it to `setup.py`:

```python
"""Package configuration."""
from setuptools import find_packages, setup

setup(
    name="pygreet",
    version="0.1",
    packages=find_packages(where="src"),
    package_dir={"": "src"},
    install_requires=["arrow"],
)
```

## (Re-)installing package also installs dependencies

Once this is saved, installing our package again (in editable mode, just as before) should bring in arrow and its dependencies.

```bash
$ pip install -e .
```

The output should include "Successfully installed arrow..." and other packages, with version numbers.

## Test

We cannot forget to write a test for our new function. In the `tests` directory, a new file called `test_greet_city.py` contains:

```python
import greet

def test_greet_city():
    result = greet.greet_city("Asia/Shanghai")
    assert "Hello, Shanghai! The time is" in result
```

Run `pytest` in the current directory. Feel proud.

## Development dependencies

So, place dependent packages _your software_ needs to run in the `install_requires` list in `setup.py`.

Dependencies _you the developer_ want in order to build your software go in `requirements.txt`.

Such as pytest. Of course, if you are installing the development dependencies you need, then you will also want the actual package you are building to be installed as well. So, let's add both in a `requirements.txt` file:

```
-e .
pytest
```

You can run this file with:

```bash
pip install -r requirements.txt
```

I like to think of `requirements.txt` this way: a list of arguments, each of which is passed to `pip`, line by line. So, running the above `requirements.txt` file is equivalent to:

```bash
pip install -e .
pip install pytest
```

Your source code tree should look something like this now:

```bash
pygreet/
├── requirements.txt
├── setup.py
├── src
│   └── greet.py
├── tests
│   ├── test_greet.py
│   └── test_greet_city.py
└── venv
```

The next time you are on a fresh system, you can download/checkout/pull your Python code, setup your virtual environment, `pip install` your requirements, and keep coding.

## Versions and upgrading

I have chosen not to specify versions of each package, or upper/lower bounds for version numbers. This way, at any time, I can

```bash
pip install -Ur requirements.txt
```

And upgrade all the packages. This works great, unless I am not paying attention to breaking API changes in any of the dependent packages. If that happens, there will be pain.

The [Python packaging guide](https://packaging.python.org/discussions/install-requires-vs-requirements/) details usage of both `install_requires` and `requirements.txt`.

You might also try `pip freeze` to see package versions. You may use the `pip freeze` output to create a `requirements.txt` file with pinned versions. That way, there will be no surprises, although your packages will grow out of date and insecure unless you update the versions.

This series is introductory in nature, and I hope you find it helpful. Please continue to explore, and enjoy, Python packaging and dependency management!

[Part 1]: https://dev.to/bowmanjd/python-dev-environment-part-1-setup-py-venv-and-pip-22gd
[Part 2]: https://dev.to/bowmanjd/python-dev-environment-part-2-testing-with-pytest-5a1h