The Great Python Package Manager Wars: A Brief and Slightly Exhausted History
andre
Python is a fine language if your heart belongs to scripting and you’d rather not wrestle a type checker into submission every Tuesday afternoon. Quick-and-dirty automation, a proof of concept you’ll swear is temporary, a small website that somehow stays up for six years. Python handles all of that with a shrug and a smile.
Now, I’ll say the quiet part: I don’t think Python is the right tool for very large, very serious engineering undertakings. But plenty of organisations disagree, or at least disagree in practice. Entire ecosystems, sprawling internal platforms, data pipelines the size of small rivers, all written in Python. The language is where it is, and where it is is everywhere.
Because Python is popular and because its learning curve is gentle enough that non-programmers pick it up on a whim, the community around it is enormous. And enormous communities produce enormous quantities of tooling. Sometimes, as we’ll see, an almost comical quantity of tooling.
Nowhere is this more obvious than in the world of package management.
The Old World
For a long time, the situation was simple, if not exactly pleasant.
1998 saw the arrival of distutils, bundled into the standard library. It let you package and distribute Python code, in the way that a hand-cranked drill lets you put a hole in a wall. Technically functional. Nobody enjoyed it.
2004 brought setuptools and its companion easy_install, which tried to make dependency resolution less of a manual archaeology dig. An improvement, certainly, though “less painful” is faint praise.
Then, in 2008, Ian Bicking released pip (“Pip Installs Packages”, a recursive acronym, because of course it is). pip was cleaner, faster, more predictable than easy_install. It became the default. If you installed Python in the last fifteen years, pip was already waiting for you, arms crossed, slightly judgemental.
Around the same period (2007), Bicking also gave us virtualenv, because installing everything into one global site-packages directory was recognised, at last, as a recipe for quiet disaster. virtualenv let you fence off a project’s dependencies from the rest of your machine. It worked. It was also, in its own way, deeply annoying to configure and remember to activate.
For a while, that was the stack: pip, virtualenv, a requirements.txt file you maintained by hand, and a prayer.
The Cracks Widen
pip’s deficiencies were well documented and loudly lamented. Dependency resolution was slow and sometimes wrong. There was no unified lock file. You couldn’t cleanly separate your runtime dependencies from your development tooling. You couldn’t pin versions with much nuance. The requirements.txt format was a flat list, essentially a shopping scribbled on the back of a receipt.
Meanwhile, the data science crowd had its own frustrations. Scientific packages often depended on compiled C or Fortran libraries, and pip handled those about as gracefully as a cat handles a bath.
So the community did what communities do. It built alternatives. Lots of them.
The Timeline of “Surely This One Will Be the Last”
2012: conda and Anaconda. Continuum Analytics (later rebranded to Anaconda, Inc.) released conda, a package and environment manager that handled binary dependencies natively. Anaconda bundled conda with a curated collection of scientific packages. For data scientists and researchers, this was genuinely transformative. You no longer needed to compile NumPy from source while questioning your life choices. The catch: conda operates in its own universe, somewhat parallel to the rest of the Python packaging ecosystem, which has caused no small amount of confusion over the years.
Also in 2012, pyenv appeared, solving the adjacent but distinct problem of managing multiple Python interpreter versions on one machine. Not a package manager per se, but it became part of the juggling act.
2017: pipenv. Kenneth Reitz, who had become something close to Python packaging royalty through the requests library, released pipenv. The pitch was seductive: pip and virtualenv unified, a proper lock file (Pipfile.lock), automatic environment creation. For a moment, it looked like the answer. Then development slowed, bugs accumulated, and the community’s patience wore thin. pipenv didn’t die so much as it quietly stopped mattering.
Also in 2017, PEP 517 and PEP 518 landed, standardising how Python projects declare their build systems. This doesn’t sound thrilling, but it was the groundwork that made everything after it possible.
2018: Poetry. Sébastien Eustace released Poetry, and it felt like a genuine step forward. A single pyproject.toml file for metadata. Proper dependency resolution. Separate groups for runtime and development dependencies. A real lock file (poetry.lock). Poetry made project configuration feel almost civilised. It gained a large and loyal following, and for several years it was the default recommendation you’d see in blog posts and conference talks.
Also in 2018, pipx arrived as a small but useful tool for installing Python CLI applications in isolated environments, sparing your global namespace from pollution.
2019: PDM. Frost Ming’s PDM adopted the emerging PEP 621 metadata standard early and offered another take on modern dependency management. Solid, well-designed, but it entered a crowded room.
2020: PEP 621 standardised project metadata inside pyproject.toml, giving tools like Poetry and PDM a common specification to target. The ecosystem was slowly, grudgingly converging on shared formats, even as the tools themselves multiplied.
2022: Hatch. Ofek Lev’s Hatch offered yet another project manager and build tool, with a plugin architecture and environment management baked in. Competent. Polite. Arrived to a crowd already fatigued by options.
2023: Rye. Armin Ronacher, the creator of Flask, released Rye, a project manager written in Rust. It aimed to be a single binary that handled Python version management, virtual environments, dependency resolution, and locking. The choice of Rust was notable and, as we’ll see, prophetic. Rye was well received but short-lived as an independent project.
2024: uv. Astral, the company behind the Ruff linter, released uv. Also written in Rust. Also absurdly fast. uv absorbed Rye’s ambitions and then some. It manages Python interpreter versions, creates virtual environments, resolves and installs dependencies, locks them, and handles both runtime and dev dependency groups. It reads and writes pyproject.toml. It produces a clear, deterministic lock file (uv.lock). And it does all of this at a speed that makes pip feel like it’s wading through cold treacle.
uv is not a Python project. It’s a Rust project that happens to manage Python packages. This is part of a broader pattern: performance-critical developer tooling increasingly written in Rust (Ruff itself, the aforementioned, being the poster child) while the language it serves remains Python. There’s something faintly amusing about that, though I won’t pretend it bothers me. Speed is speed.
The Cost of Abundance
Every one of these tools arrived with a reasonable justification. pip was too limited, so here’s pipenv. pipenv stalled, so here’s Poetry. Poetry doesn’t manage Python versions, so here’s Rye. Rye got absorbed, so here’s uv. conda serves a different constituency but overlaps enough to create confusion at the boundaries.
And with each new darling, the community fractures a little further. Tutorials reference the old tool. CI configurations need rewriting. Onboarding documents go stale within eighteen months. A junior developer joins a team and has to learn not just Python but also whichever packaging philosophy the team lead happened to prefer in 2019.
The churn is exhausting, and it’s not purely a technical problem. It’s a social one. Every new tool fragments attention, splits documentation efforts, and forces a migration conversation that nobody particularly wants to have.
A Cautious Resting Place
I’ll admit a bias: I like uv. I think it’s the most complete answer the ecosystem has produced so far. It covers the things that actually matter for a project of any meaningful size.
You get clear, expressive version constraints. Upper bounds, lower bounds, exact pins, compatible-release operators. You get separate dependency groups for runtime and development, so your production image isn’t lugging around pytest and a linter. You get a lock file that is human-readable enough to review in a pull request and deterministic enough to trust in a build pipeline. And you get Python interpreter management baked in, which makes uv particularly well suited to containerised deployments where you want a single tool to provision the runtime and the packages without a five-step shell script.
Is uv the final answer? I suspect it’ll hold the position for a good while. Astral has funding, momentum, and a track record of shipping fast, focused tools. The Rust foundation means performance won’t be the bottleneck that forces yet another rewrite.
But I’ve been wrong before, and the Python community has a long, proud tradition of building a replacement for the replacement of the replacement. So I’ll keep my enthusiasm measured. Enjoy uv while it’s the fashion. And if something shinier lands next February, well. We’ve all been through this before.
At least the pyproject.toml format will probably survive. Probably.