Packaging and Dependency Hell
Maven gives you a contract: declare your dependencies in pom.xml, run mvn install, and reproducible builds fall out of the other end — lockfile semantics baked in, a central repository, and a well-understood lifecycle. Python's packaging story is older, messier, and in active recovery. If you have watched a colleague run pip install -r requirements.txt only to get a subtly different environment than the one that worked in CI, you have seen the problem first-hand.
This post maps Maven concepts to the Python toolchain so you can navigate the ecosystem without starting from scratch.
The Ecosystem at a Glance
The key insight: pyproject.toml is the pom.xml. Everything else — the tool you invoke, the lockfile format, the virtual environment manager — is pluggable around it.
Virtual Environments: The Classpath Problem
The JVM isolates dependency versions through classloaders and module-path flags. Python's equivalent is the virtual environment — a directory containing a private Python interpreter symlink and a site-packages folder.
# Create and activate a venv (built-in since Python 3.3)
python -m venv .venv
source .venv/bin/activate # macOS/Linux
# .venv\Scripts\activate # Windows
python -m pip install requestsWithout a venv, pip install writes to the system Python's site-packages — the equivalent of installing jars into the JRE's lib/ext. Don't do it.
pyproject.toml — The pom.xml
A minimal pyproject.toml:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-service"
version = "1.0.0"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.110,<1.0",
"httpx>=0.27",
"pydantic>=2.6",
]
[project.optional-dependencies]
dev = ["pytest>=8", "mypy>=1.9", "ruff>=0.4"]Compare to Maven:
Maven (pom.xml) |
Python (pyproject.toml) |
|---|---|
<groupId>/<artifactId> |
[project] name |
<dependency> (compile) |
dependencies = [...] |
<dependency> (test) |
[project.optional-dependencies] dev |
<build><plugins> |
[tool.hatch.build] or equivalent |
<properties> |
[tool.*] sections |
Version specifiers use PEP 440 syntax: >=0.110,<1.0 is equivalent to Maven's [0.110, 1.0). The ~= operator is the Python equivalent of the Maven ~ patch-level range: ~=1.4.2 means >=1.4.2, <1.5.
pip — The Basic Tool
pip is the built-in installer. It resolves dependencies and installs wheels, but it does not produce a lockfile by default.
pip install fastapi # install latest
pip install "fastapi>=0.110" # with constraint
pip install -r requirements.txt # install from file
pip freeze > requirements.txt # snapshot current envpip freeze produces a pinned requirements.txt — close to Maven's dependency:tree flattened into a reproducible file, but fragile because it is generated from the current environment, not derived from a resolver.
Poetry — Maven-Like Workflow
Poetry adds what Maven provides out of the box: a proper lockfile (poetry.lock), a virtual environment managed automatically, and build + publish commands.
poetry new my-service # scaffold project
poetry add fastapi # add dependency
poetry add --group dev pytest # add dev dependency
poetry install # install from lockfile
poetry run pytest # run in managed venv
poetry build # build wheel
poetry publish # publish to PyPIThe poetry.lock file pins every transitive dependency with its hash — equivalent to Maven's dependency resolution + checksum verification. Commit it to version control.
uv — The Fast Path
uv (from Astral) is a Rust-based resolver and installer that is 10–100x faster than pip for large dependency trees. It speaks the same pyproject.toml and requirements.txt formats, so it is a drop-in for most workflows.
uv venv # create .venv
uv pip install fastapi # fast install
uv pip compile pyproject.toml -o requirements.lock # lockfile
uv pip sync requirements.lock # install from lockfileIn CI, swapping pip install for uv pip install is often the single highest-leverage performance change for build times — akin to switching from Maven's default resolver to Gradle's parallel dependency resolution.
Dependency Conflicts — The Real Hell
The "dependency hell" in the title refers to version conflicts that pip's old resolver silently ignored (installing an incompatible version). The modern pip resolver (backtracking, since pip 20.3) raises an error instead:
ERROR: Cannot install package-a==1.0 and package-b==2.0
because these package versions have conflicting dependencies.Resolution strategies:
- Use Poetry or uv — their resolvers are smarter and give better error messages.
- Pin transitive dependencies explicitly in
requirements.txt/ lockfile. - Check for upper-bound constraints that are too conservative (
package-c<2.0when 2.x is compatible).
This is the same problem as Maven dependency mediation — Maven picks the nearest version (first-wins); pip's resolver raises conflicts rather than silently choosing. Neither is perfect, but explicit errors are better than silent mismatches.
Key Takeaways
pyproject.tomlis thepom.xml: declare metadata, dependencies, and tool configuration in one file.- Always use a virtual environment — it is Python's answer to isolated classpaths;
python -m venv .venvis sufficient for most projects. pip freezeproduces a snapshot, not a true lockfile — use Poetry or uv for reproducible builds.- Poetry maps closely to the Maven mental model: lockfile, managed venv, build, and publish in one tool.
- uv is the fastest resolver/installer available today; swap it into any pip-based workflow for free speed gains.
- Version specifiers follow PEP 440:
>=,<,~=map cleanly to Maven range notation once you learn the syntax.