Managing Python Dependencies to Prevent Project Deployment Issues
Understanding dependencies, lock files, and modern package management using uv
Difference between packages, library and dependencies
Before discussing package management and dependency handling in a Python project, it helps to understand the basic building blocks Python uses to organise and reuse code. The Python documentation clearly defines modules, packages, and imports, but it does not explicitly define libraries or dependencies, even though these terms are commonly used in everyday development discussions. Understanding how these ideas relate to one another makes it easier to reason about project structure and dependency management.
Modules
A module is the simplest unit of organisation in Python. It is essentially a single Python file that contains code such as functions, classes, or variables.
Modules exist to organise code logically. Instead of writing everything in a single large file, you break your program into smaller files, with each module handling a specific responsibility.
Example:
# math_utils.py
def add(a, b):
return a + b
def subtract(a, b):
return a - bThis file is a module. Another Python file can use it by importing it.
import math_utils
result = math_utils.add(2, 3)At the most basic level, modules are how Python organises code into manageable units.
Packages
A package is a collection of modules grouped into a directory so they can be organised and reused as a single unit.
Packages make it easier to structure larger projects and to distribute code that performs a specific function. If modules are individual files, packages are folders that contain related modules.
The key idea behind a package is shareability and reuse. Packages allow code that implements a specific functionality to be packaged in a way that others can install and import into their own projects.
Example package structure:
my_package/
__init__.py
math_utils.py
string_utils.pyYou can then import from that package:
from my_package import math_utilsIn practice, packages are how most third-party functionality is distributed in Python. When you install something using pip, you are usually installing a package.
Example:
pip install django-debug-toolbarAfter installing it, you can import it into your project because the package is now available in your Python environment.
So a useful way to think about it is:
Modules organise code inside a file
Packages organise modules into a reusable structure
Libraries
A library is generally a broader collection of packages and modules that provide related functionality.
Unlike the previous terms, “library” is not a strict technical construct defined by Python itself. It is more of a conceptual grouping used by developers to describe a large body of reusable code.
For example, Python ships with what is commonly called the standard library, which includes modules and packages that come pre-installed with Python.
Examples from the Python standard library include:
datetimefor working with dates and timesjsonfor working with JSON dataosfor interacting with the operating systemmathfor mathematical operations
Example usage:
import datetime
now = datetime.datetime.now()You do not need to install these because they come bundled with Python.
So in simple terms:
A package is a structured unit of code distribution
A library is a broader collection of packages and modules that provide related capabilities
Libraries often contain multiple packages.
Dependencies
A dependency is anything a project relies on to run correctly.
This term is less about code structure and more about project requirements. When we say a project has dependencies, we are referring to external packages or libraries that must be present for the application to function.
For example, if a web application uses Django and requests, those packages must exist in the environment before the application can run.
Example dependency list:
Django==4.2
requests==2.31.0
psycopg2==2.9.7These dependencies are often recorded in files such as:
requirements.txtpyproject.tomlPipfile
This allows other developers to recreate the same environment.
Example:
pip install -r requirements.txtIn that sense, dependency is a contextual term. It describes the relationship between your project and the external code it needs.
Putting It All Together
These concepts are closely related but operate at different levels.
Module: A single Python file containing code.
Package: A directory of modules organised so they can be reused and distributed.
Library: A broader collection of packages and modules that provide related functionality.
Dependency: Any external library or package that your project requires to run.
So when working on a real Python project, the flow often looks like this:
Developers write modules.
Modules are grouped into packages.
Packages may belong to a larger library.
When your project uses that library, it becomes a dependency of your project.
Managing your project packages efficiently
What actually makes a good package management workflow in Python?
There are many tools available today. Some are older and widely used, such as pip. Others are newer and attempt to solve problems that developers have historically struggled with. One of the most recent tools gaining attention is uv.
To reason about which tools are better, it helps to define the criteria that matter.
1. Usability
The first consideration is usability.
A good package management tool should be easy to understand and easy to use. The commands required to initialise a project, install dependencies, or update packages should not be overly complex.
Good documentation is also critical. Developers will inevitably run into issues such as dependency conflicts or installation errors. When that happens, clear documentation makes the difference between solving the issue quickly and spending hours debugging environment problems.
In practice, tools that require fewer steps and clearer commands tend to reduce friction in development workflows.
2. Virtual Environment Management
Another important factor is how the tool handles virtual environments.
Virtual environments isolate project dependencies so that different projects can use different packages or versions without interfering with each other.
For many beginners, this is one of the first confusing parts of Python development. It is very common to see developers install packages globally without realising that different projects may require different versions of the same dependency.
Some tools expect the developer to create and manage virtual environments manually. Others automatically create and manage environments as part of their workflow.
Understanding how a tool handles this isolation is important because environment issues are one of the most common causes of broken Python setups.
3. Python Version Management
Python version compatibility is another critical consideration.
Different packages support different Python versions. A dependency that works perfectly with Python 3.11 might not work with Python 3.8, and vice versa.
A good package management tool should allow a project to clearly define which Python versions it supports. This ensures that the environment used during development matches the environment used in production.
Without this control, it becomes very easy to accidentally create setups that work locally but fail when deployed elsewhere.
4. Dependency Management
The core responsibility of a package management workflow is tracking dependencies.
A project should have a clear and easily readable definition of the packages it depends on and their versions.
Developers should be able to add, remove, and inspect dependencies without manually editing configuration files or trying to infer what was installed previously.
Historically, many Python projects used a requirements.txt file for this purpose. However, tools like uv now rely on pyproject.toml, which has become the modern standard for defining project metadata and dependencies.
5. Reproducibility
Reproducibility is one of the most important properties of a production grade system.
When a developer clones a repository and installs its dependencies, the environment they create should behave exactly the same as the environment used by the original developers.
Without reproducibility, applications may work today but fail weeks or months later when dependency versions change.
This is where lock files become essential.
6. Collaboration
Finally, package management workflows should make collaboration straightforward.
Developers working on different operating systems such as macOS, Linux, or Windows should all be able to recreate the same environment.
A well defined dependency configuration and lock file ensure that every collaborator installs the exact same dependency graph, preventing the classic “it works on my machine” problem.
Why Modern Tools Like uv Are Gaining Popularity
Traditionally, Python developers relied on pip to install packages.
pip works well as a package installer, but it was never designed to manage an entire dependency workflow. As a result, developers often had to combine multiple tools together:
pip for installing packages
venv for creating virtual environments
requirements.txt for tracking dependencies
This workflow works, but it requires manual steps.
For example, when using pip, developers often regenerate the dependency list manually using:
pip freeze > requirements.txtThis command captures the packages installed in the environment and writes them into the requirements file.
Modern tools like uv streamline this process.
uv integrates several pieces of functionality into a single workflow:
installing packages
managing virtual environments
resolving dependencies
maintaining dependency definitions
generating lock files
It also performs dependency resolution extremely quickly. Because uv is implemented in Rust, it can resolve dependencies dramatically faster than traditional Python based tooling, often by an order of magnitude.
Instead of manually updating dependency files, uv automatically records project dependencies in the pyproject.toml file when new packages are added.
For example:
uv add fastapiThis command both installs the dependency and updates the project’s dependency definition.
Why Lock Files Matter
One of the most important features introduced by modern dependency managers is the lock file.
A dependency definition file such as pyproject.toml usually describes the acceptable versions of dependencies.
For example:
fastapi >=0.110This means any compatible version of FastAPI can be installed.
However, dependencies themselves have their own dependencies. Over time, newer versions of those packages may be released. This can lead to different combinations of packages being installed at different times.
A project that worked perfectly today might fail months later because one of the indirect dependencies has changed.
A lock file solves this problem.
Instead of describing acceptable ranges, the lock file records the exact versions of every dependency in the entire dependency graph, including transitive dependencies.
For example:
fastapi 0.110.0
pydantic 2.6.1
typing_extensions 4.9.0Because these versions are pinned, every environment created from the lock file will install the exact same package versions.
This guarantees that the environment used by one developer will match the environment used by another developer, as well as the environment used in production.
In uv, this information is stored in the uv.lock file.
When this file is committed to version control, anyone working on the project can recreate the same environment deterministically.
This dramatically reduces environment related bugs and makes Python applications far more reliable to build and maintain.
Actually Using uv in a Project
After understanding why dependency management matters and why modern tools like uv are becoming popular, the next step is very simple: actually using it in a project.
One of the things I like about uv is that the workflow is straightforward. The commands are minimal, and the tool handles a lot of the environment and dependency management automatically.
Initialising a Project
The first step is to initialise uv inside your project directory.
uv initRunning this command sets up the project for dependency management. It creates the necessary project configuration so that uv can track dependencies and manage the environment for the project.
In most cases, this will create or configure the pyproject.toml file, which is where the project metadata and dependency definitions live.
Adding Dependencies
Once the project is initialised, dependencies can be added using the uv add command.
uv add fastapiThis installs the package and automatically records the dependency in the project’s configuration.
At the same time, uv generates a lock file called:
uv.lockThis file captures the exact versions of all dependencies, including transitive dependencies. Because of this, the environment can be recreated exactly the same way across different machines.
Unlike older workflows where dependency files often needed to be manually updated, uv updates the project definition automatically when new packages are added.
Grouping Dependencies
Another useful feature is dependency grouping.
Not all dependencies serve the same purpose. Some are required for running the application, while others are only needed for development tasks such as testing or linting.
uv allows dependencies to be grouped so that they are organised more clearly.
For example:
uv add --group dev pytestThis adds pytest to a development dependency group instead of the main runtime dependencies.
Structuring dependencies this way keeps the project configuration cleaner and makes it easier to understand which packages are required for which part of the workflow.
Running Commands with uv
To run commands within the project’s managed environment, uv provides the uv run command.
uv run python app.pyThis runs the command using the environment that uv manages for the project. All dependencies defined in the project configuration and lock file are available when the command executes.
This removes the need to manually activate virtual environments before running scripts. uv ensures the correct environment is used automatically.
Why This Workflow Works Well
What stands out to me about uv is how it simplifies several steps that historically required multiple tools.
Instead of juggling environment creation, package installation, dependency tracking, and lock files separately, uv brings these pieces together into a single workflow.
The result is a setup that is easier to reason about, easier to reproduce, and much less prone to the environment issues that Python developers have traditionally struggled with.
For a production project where reproducibility and collaboration matter, that is a significant improvement.
Glossary and Definitions
Based on Offical Python docs.
namespace: The place where a variable is stored. Namespaces are implemented as dictionaries. There are the local, global and built-in namespaces as well as nested namespaces in objects (in methods). Namespaces support modularity by preventing naming conflicts.
importing: The process by which Python code in one module is made available to Python code in another module.


I love using uv compared to pip cause its very fast and was built with Rust programming language