3  Handling dependencies

Dependencies are the external Python packages your code needs in order to work, such as requests, numpy, or pandas.

Here we’ll focus on using uv to handle dependencies, as it’s currently the best tool for this out there (it’s fast and fairly easy to use, especially if you know pip).

3.1 Specify dependencies

For example, let’s say we have this function in our package:

import numpy as np

def normalize(array):
   min_val = np.min(array)
   max_val = np.max(array)
   return (array - min_val) / (max_val - min_val)

When people want to use our function, they need to have numpy installed for it to work, otherwise it will raise a ModuleNotFoundError on their machine.

So in order to ensure is numpy installed, we set numpy as a dependency of our package. This means that every time someone install our package, they will also install numpy.

The dependencies of a package are listed in the pyproject.toml file. If you don’t know what that is, check out organizing a package.

With uv, we just have to run:

uv add numpy

This will automatically add numpy to our pyproject.toml.

[project]
name = "mypackage"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []
[project]
name = "mypackage"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
    "numpy>=2.2.4",
]

If we add other dependencies, they will be added to the dependencies list.

3.2 Avoiding dependencies

In general, we want to avoid having too many dependencies. Why is that? Because when we install a package, we need to install its dependencies too, as well as the dependencies of those packages, and so on.

The issue with this is that it adds a lot complexity quickly and increase the risk of having conflicts.

For example, one package might need a version of numpy before <2.0.0, while another need a version above or equal to >=2.2.0. This kind of situation can quickly arise if not careful when adding too many dependencies, and it’s usually a nightmare to resolve.

Note: packages with low or no dependencies are called lightweight. As an example, have a look at the narwhals package.

The thing with having lots of dependencies is that it makes it easier for you to write code because you can use other people code super easily. So it’s always a trade-off somehow.

Always ask yourself those questions before adding a new package to your dependencies:

  • is the dependency a well-known, stable package (numpy, requests, etc) or is it new and is likely to change in the future?
  • does this dependency has lots of dependencies too? This might be a red flag 🚩
  • can’t you just code what it does yourself? If you only need a single function, go check their source code on Github and see if it’s easy to do on your side (and ensure their License allow you to copy the code too).

3.3 Controlling the versions

What’s the difference between numpy 2.1.0 and numpy 2.0.0? Well, many things, but for example, in numpy 2.0.0, the np.unstack() function doesn’t exist as it’s a new one from numpy 2.1.0.

If our package relies on np.unstack() in one of our functions, we can’t let people install any numpy version when installing our package. We need to ensure people install this version: numpy>=2.1.0. If we translate it, it means any version of numpy above or equal to 2.1.0. Let’s see some other examples.

Install exactly this version of numpy.

uv pip install numpy==2.1.0

Install the latest available version before 2.1.0 (including 2.1.0) of numpy.

uv pip install 'numpy<=2.1.0'

Install the latest version between 2.1.0 (included) and 2.2.0 (excluded) of numpy.

uv pip install 'numpy>=2.1.0,<2.2.0'

Install the latest version of numpy.

uv pip install numpy

Note that for each of those, the package resolver will always try to install the latest version it can depending on the other dependencies. If a package requires numpy<=2.1.0, other packages must include numpy 2.1.0 for it to work.

At this point, you might ask, how do I know which versions of each dependencies are required for my package? Well, as far as I know, there is no easy answer to this, but there are ways to ensure you don’t get unexpected behaviors.

3.3.1 Set the minimum version required…

For each of your dependencies, set in your pyproject.toml the minimum version required. With uv, you can run the following to install a specific version:

uv pip install numpy==2.0.0

Warning: the command above will install a specific version of numpy, but will not change the requirements in pyproject.toml. Use uv add if you want to change them.

3.3.2 … test your code…

You have to test that your code works as expected on those versions. The best way to do that is unit testing, and it’s the point of the next blog post.

3.3.3 … and be convenient

Dependening on whether you’re planning on distributing your package (e.g., put it on PyPI and allow other people to install it) or not, you might want to do different things here. We’ll assume you want to distribute it at the end.

When your package goaled is to be installed by other people, you want to be convenient. By that I mean not being too restrictive.

If we take our example from before, we know that we need at least numpy==2.1.0 for our package to work, but we also know that any numpy version above works too. For this reason, we set numpy>=2.1.0 instead of numpy==2.1.0 to allow a broader range of possibility.

3.4 Breaking changes

By default, when installing our package, it will try to find the latest numpy version that satisfies the requirements.

But, you might say there’s a risk it will break on a new numpy version? Yes, it absolutely does. And that’s exactly why we said earlier why we wanted to avoid having too many dependencies and use stable ones only.

The good thing with packages like numpy is that it’s one of the most important Python package and one of its core component. They can’t make breaking changes on any significant feature. When they want to do it, they usually add warnings like this: “The function xxx is deprecated and will be removed in a future version, please use yyy instead?”.

But, if you want to be sure you don’t get breaking changes, set the maximum version of the dependencies, with things like numpy<=2.2.0. This will ensure it’s safe, but this also means you’ll need to manually update it as new versions come out.

3.5 Required, Optional and Dev dependencies

When working with dependencies, it’s useful to differentiate between three main types: required, optional, and development dependencies. Each serves a different purpose in your package.

3.5.1 Required

Required dependencies are the ones we’ve been discussing so far - packages that your code absolutely needs to function properly. These go in the dependencies list in your pyproject.toml.

[project]
dependencies = [
    "numpy>=2.1.0",
    "pandas>=2.0.0",
]

3.5.2 Optional dependencies

Optional dependencies are packages that enhance your code but aren’t strictly necessary for core functionality. For example, if your data processing package works with CSV files by default but can also handle Excel files with an additional dependency.

You can specify these in your pyproject.toml using the [project.optional-dependencies] section:

[project.optional-dependencies]
excel = ["openpyxl>=3.1.0"]
plot = ["matplotlib>=3.7.0", "plotly>=5.23.0"]

This lets users install only what they need:

Install your package required dependencies only:

uv pip install mypackage

Install your package required dependencies as well openpyxl:

uv pip install "mypackage[excel]"

Install your package required dependencies as well matplotlib and plotly:

uv pip install "mypackage[plot]"

Install with all optional dependencies:

uv pip install "mypackage[excel,plot]"

In your code, you’ll need to handle cases where optional dependencies aren’t installed:

def read_file(filename):
    if filename.endswith('.csv'):
        # Core functionality
        import pandas as pd
        return pd.read_csv(filename)
    elif filename.endswith('.xlsx'):
        try:
            # Optional functionality
            import openpyxl
            import pandas as pd
            return pd.read_excel(filename)
        except ImportError:
            raise ImportError(
                "Excel support requires 'openpyxl'. "
                "Install with 'pip install mypackage[excel]'"
            )

This will give your users a clear and meaningful error message that they can resolve very quickly. This kind of thing exist for the same reason we’re talking about in this article: trying to minimize the number of dependencies (especially the unused ones!).

In order to add package to your optional dependencies, you can run:

uv add matplotlib --optional plot

This will add matplotlib to the plot section in the optional dependencies in your pyproject.toml.

[project]
name = "mypackage"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []
[project]
name = "mypackage"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []

[project.optional-dependencies]
plot = ["matplotlib>=3.7.0"]

3.5.3 Dev dependencies

Development dependencies are packages you need only when developing your package, not when using it. These include testing frameworks, documentation generators, linters, and similar tools.

Specify these in your pyproject.toml like this:

[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "ruff>=0.11.5",
    "sphinx>=7.0.0",
]

In order to add a package to your dev dependencies, you can run:

uv add ruff --dev

Developers working on your package can install all optional dependencies as well as dev dependencies with:

uv sync --all-groups