Dependency management is how a Python project records which third-party packages it needs, which versions are acceptable, and exactly which versions were installed, so that anyone can recreate a working environment. It builds on pip and virtual environments, which isolate each project's packages.
The modern standard is a pyproject.toml file at the project root:
[project]
name = "booking-service"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"requests>=2.31,<3",
"python-dateutil>=2.8",
]This lesson explains project metadata, version specifiers, the difference between a specification and a lock file, reproducible set-ups, and separating development tools from runtime needs. The examples manage a small booking service for a climbing wall.
Project metadata
pyproject.toml (defined by PEP 518 and PEP 621) describes the project itself: its name, version, supported Python versions, direct dependencies and how to build it. Tools such as pip, build backends, linters and test runners read it, so configuration that used to be spread across setup.py, setup.cfg and tool-specific files can live in one place.
Keep two things separate in your mind:
- What the project needs: the direct dependencies you import, with acceptable version ranges. This belongs in
pyproject.toml(or a short hand-writtenrequirements.in). - What an environment contains: every installed package, including dependencies of dependencies, at exact versions. This belongs in a generated lock file.
requirements.txt from pip freeze is a simple lock file. Tools such as pip-tools (requirements.txt from requirements.in), Poetry (poetry.lock), PDM (pdm.lock) and uv (uv.lock) generate richer ones. Each has its own workflow; pick one per project and document it in the README.
Version constraints
Version specifiers say which releases are acceptable:
| Specifier | Meaning |
|---|---|
requests==2.32.3 | exactly this version |
requests>=2.31 | 2.31 or newer, including future major versions |
requests>=2.31,<3 | at least 2.31, below 3.0 |
requests~=2.31 | compatible release: >=2.31, ==2.* |
requests!=2.30.0 | anything except a known-broken release |
Most libraries follow semantic versioning loosely: major versions (3.0) may break compatibility, minor and patch versions shouldn't. A range like >=2.31,<3 accepts fixes and features while avoiding the next breaking release.
A range is a policy, not a snapshot. requests>=2.31,<3 can install 2.31 today and 2.40 next year. That's what you want for a library, so it works alongside other packages, but an application also needs a lock file so that every deployment uses the same tested versions.
Reproducibility
A project is reproducible when a fresh checkout can be turned into a working environment by following written steps. Record three things:
- The interpreter:
requires-pythoninpyproject.toml, and often a.python-versionfile for tools such as pyenv and uv. - The dependency inputs:
pyproject.tomland the lock file, both committed to Git. - The instructions: a short README section.
python -m venv .venv
.venv/bin/python -m pip install -r requirements.txt # exact versions from the lock
.venv/bin/python -m pip install -e . # the project itself, editableNever rely on copying a .venv folder: it contains absolute paths and platform-specific binaries. Continuous integration that builds the environment from scratch on every push is the best proof the instructions are complete. Update dependencies deliberately, regenerate the lock, run the tests, and commit the new lock file on its own so the change is easy to review.
Development dependencies
Tools for testing, linting, type checking and documentation, such as pytest, Ruff, mypy and Sphinx, are needed by developers but not by the running application. Keep them separate so production installs stay small and secure:
[project.optional-dependencies]
dev = ["pytest>=8", "ruff>=0.6", "mypy>=1.10"]Install with python -m pip install -e ".[dev]". Python packaging also standardised dependency groups (PEP 735) under [dependency-groups], supported by newer tools including uv and recent pip versions. Poetry uses [tool.poetry.group.dev.dependencies], and pip-tools projects often keep a separate requirements-dev.in.
The syntax differs between workflows, which is one more reason to choose a single tool per project and document the exact commands. Whatever the syntax, the principle is the same: runtime dependencies in one list, development tools in another.
Independent practice and a worked check
Task: document a booking project interpreter and dependency workflow, then recreate it in a fresh environment.
# pyproject.toml
[project]
name = "booking"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = ["requests>=2.31,<3"]
[project.optional-dependencies]
dev = ["pytest>=8"]python -m venv .venv
.venv/bin/python -m pip install -e ".[dev]"
.venv/bin/python -m pip freeze --exclude-editable > requirements.lock
# later, on a clean checkout:
python -m venv .venv2
.venv2/bin/python -m pip install -r requirements.lock -e .On Windows, use .venv\Scripts\python.exe. Comparing pip freeze in both environments should show identical versions. For the worked check, a loose version constraint guarantees a permitted version range, not one exact version.
Reference and next practice
The Python Packaging User Guide explains writing pyproject.toml and version specifiers. Continue with Git and Python projects. Practise with the Python exercises and Python quiz.
Virtual Environments and Dependencies Summary
- Project metadata: pyproject.toml can describe a package and its declared dependencies using the chosen build tool.
- Version constraints: A dependency specification defines accepted versions while a lock records a particular resolution.
- Reproducibility: Record the interpreter, dependency inputs and installation instructions.
- Development dependencies: Test and analysis tools can be separated from application runtime requirements.
Virtual Environments and Dependencies Examples
Each example shows the code, the output it produces and what each part does.
Virtual Environments and Dependencies in a small program
This checks what the current environment provides. The output depends on your set-up: it prints pytest's version if it's installed, otherwise the "not installed" message, followed by the Python major version (3).
import sys
from importlib.metadata import version, PackageNotFoundError
try:
print(version("pytest"))
except PackageNotFoundError:
print("pytest is not installed in this environment")
print(sys.version_info.major)Line-by-line explanation
- Line 2
importlib.metadatareads installed package information without importing the package. - Lines 3 to 6A missing package raises
PackageNotFoundError, which is reported instead of crashing. - Line 7
sys.version_info.majorconfirms the interpreter's major version.
Checking an environment against requirements
- Real projects use
packaging.version.Versionfor full version comparisons; this example keeps to the standard library.
from importlib.metadata import version, PackageNotFoundError
requirements = {"pip": (20, 0), "surely-missing-pkg": (1, 0)}
def major_minor(text):
parts = text.split(".")
return int(parts[0]), int(parts[1]) if len(parts) > 1 else 0
for name, minimum in requirements.items():
try:
installed = major_minor(version(name))
except PackageNotFoundError:
print(f"{name}: MISSING (need >= {minimum[0]}.{minimum[1]})")
continue
status = "ok" if installed >= minimum else "TOO OLD"
print(f"{name}: {status}")pip: ok
surely-missing-pkg: MISSING (need >= 1.0)Line-by-line explanation
- Line 3A tiny stand-in for a requirements file: package names with minimum versions.
- Lines 5 to 7Comparing (major, minor) tuples orders versions numerically, so 2.10 counts as newer than 2.9.
Common Virtual Environments and Dependencies Mistakes
Treating a loose range as a reproducible environment
# requirements.txt
requests
flask>=2# pyproject.toml declares ranges; a generated lock pins what was tested:
# requests==2.32.3
# flask==3.0.3
# (plus every transitive dependency at an exact version)Unpinned requirements can install different versions on different days. Keep ranges for intent and a lock for reproducibility.
Mixing development tools into runtime dependencies
dependencies = ["requests>=2.31", "pytest>=8", "ruff>=0.6"]dependencies = ["requests>=2.31"]
[project.optional-dependencies]
dev = ["pytest>=8", "ruff>=0.6"]Virtual Environments and Dependencies in Real Projects
Dependency workflows in practice
Teams commit pyproject.toml and a lock file, rebuild environments in continuous integration, and use bots such as Dependabot or Renovate to open pull requests when new versions appear, so updates are tested before they're merged. Security scanners such as pip-audit check locked versions against known vulnerabilities.
uv has quickly become popular for its speed: uv add requests updates pyproject.toml and uv.lock, and uv sync makes the environment match the lock exactly. Poetry and PDM offer similar all-in-one workflows, while pip with pip-tools remains common in projects that prefer minimal tooling.
Virtual Environments and Dependencies FAQ
Should I use requirements.txt or pyproject.toml?
Use pyproject.toml to declare your project and its direct dependencies; it's the current standard. A requirements.txt generated by pip freeze or pip-tools still works well as a lock file of exact versions for deploying an application.
What is a lock file?
A generated file listing every package in a working environment, including indirect dependencies, at exact versions, often with hashes. Installing from it reproduces the tested environment precisely. Examples include poetry.lock, uv.lock and a pinned requirements.txt.
What does ~= mean in Python requirements?
It's the "compatible release" operator. ~=2.31 means >=2.31 and ==2.*, so it accepts 2.32 or 2.40 but not 3.0. ~=2.31.1 means >=2.31.1 and ==2.31.*.
Should libraries pin exact versions?
No. Libraries should declare ranges that they support, so they can be installed alongside other packages. Exact pins belong in applications' lock files, where the goal is a single reproducible environment.
Is Poetry or uv better than pip?
They solve the same problem with different trade-offs. pip with a requirements file is simple and universal; Poetry, PDM and uv add automatic locking, dependency groups and environment management, and uv is particularly fast. Any of them is fine if the whole team uses the same one.
Virtual Environments and Dependencies Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
What does a loose version constraint guarantee?
Which review concern is valid for virtual environments and dependencies?
Virtual Environments and Dependencies Quiz
2 questions to check what you learned.
What does a loose version constraint guarantee?
- One exact resolved environment
- A permitted version range
- Python ignores the distinction
Which statement correctly describes reproducibility?
- Record the interpreter, dependency inputs and installation instructions.
- Only the example output matters, not the rule
- Every operation has the same behavior