How Python Package Management Really Works
Understand what happens when you run `pip install`, why virtual environments matter, and how modern tools like Poetry and lock files solve real-world dependency conflicts.
How Python’s Package Management Really Works (And Why It Trips Everyone Up)
If you’ve ever typed pip install and watched the magic happen, you’ve probably wondered: What’s actually going on under the hood? You’re not alone. Python’s package management can feel like a maze of .toml files, virtual environments, and dependency conflicts that make you want to throw your laptop out the window.
But here’s the thing—it’s not as complicated as it seems. Once you understand the core ideas, everything clicks. Let’s break it down in plain English, the PythonSkillset way.
The Foundation: What Even Is a Package?
Think of a Python package as a zip file with superpowers. It contains:
- Python modules (.py files)
- Metadata (name, version, dependencies)
- Sometimes compiled code (C extensions, for example)
When you run pip install requests, pip downloads that zip file, extracts it, and puts the modules where Python can find them. Simple enough, right?
But here’s where people get tripped up: where does it put them?
The Global vs. Virtual Environment Problem
By default, pip installs packages to your system Python’s site-packages directory. On Linux, that’s usually something like /usr/lib/python3.10/site-packages/.
This works fine... until you need different versions for different projects. Then you hit dependency hell.
PythonSkillset learned this the hard way. We had one project that required numpy 1.21 and another that needed numpy 1.26. Guess what? You can't have both in the same global site-packages.
That’s why virtual environments exist.
Virtual Environments: Your Projects’ Private Playground
A virtual environment is basically a copy of Python with its own site-packages. Something like this:
python -m venv my_project_env
This creates a folder called my_project_env with:
- A Python binary
- A pip binary
- An empty site-packages directory
When you activate it (source my_project_env/bin/activate on Linux/macOS or my_project_env\Scripts\activate on Windows), any pip install goes into that isolated space. Project A can have numpy 1.21, Project B can have numpy 1.26, and they never fight.
How Pip Actually Resolves Dependencies
Here’s where it gets interesting. When you run pip install flask, pip doesn't just grab Flask. It:
1. Reads Flask’s dependencies from its metadata (say, Werkzeug>=2.0 and Jinja2>=3.0)
2. Downloads those too
3. Checks if those packages have dependencies (Werkzeug might need MarkupSafe)
4. ...and so on, recursively
This is called dependency resolution. Modern pip (version 20.3+) uses a dependency resolver that tries to find a set of package versions that satisfy all constraints simultaneously.
But what if two packages conflict? Like Package A needs requests<2.28 and Package B needs requests>=2.28? The resolver will tell you “I can’t satisfy these requirements” and refuse to install. That’s better than the old pip, which would silently install conflicting versions and cause runtime errors.
Beyond Basic Pip: Why Requirements Files Aren’t Enough
Most PythonSkillset readers know about requirements.txt:
flask==2.3.0
requests==2.31.0
But here’s the dirty secret: that only pins your top-level dependencies. Your transitive dependencies (like Werkzeug or urllib3) are free to upgrade. This leads to the classic problem: “It works on my machine but not in production.”
That’s where lock files come in. Tools like pip freeze capture the exact versions of every package installed:
flask==2.3.0
werkzeug==2.3.0
jijna2==3.1.2
markupsafe==2.1.3
...
Save this to requirements.txt and you get reproducible builds. But there’s a better way.
Modern Approach: pyproject.toml and Lock Files
The Python Packaging Authority (PyPA) has standardized on pyproject.toml for project metadata. Here’s what a modern Python project looks like:
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.backends._legacy:_Backend"
[project]
name = "my_app"
version = "1.0.0"
dependencies = [
"flask>=2.0",
"requests>=2.28",
]
This is cleaner than setup.py and requirements.txt. But for truly reproducible builds, you still need a lock file. Tools like Poetry, Pipenv, or PDM generate these automatically.
Why Pip Install Isn’t Always the Right Tool
Don’t get me wrong—pip is great for quick installations. But for serious projects, you want:
- Deterministic builds (lock files)
- Semantic versioning awareness
- Dependency groups (dev, test, production)
That’s why many PythonSkillset tutorials now use Poetry for new projects. It manages virtual environments, resolves dependencies, and generates lock files—all with one tool.
The Real-World Pitfall: Platform-Specific Packages
Here’s something that catches everyone: some packages have platform-specific wheels. For example, numpy has different wheels for Windows, macOS (Intel vs Apple Silicon), and Linux. When you share your requirements.txt with a teammate on a different OS, pip might install different versions.
Solution? Always test on the same platform you deploy to, and use lock files that include platform markers.
Wrapping Up (Without the Nonsense)
Python’s package management isn’t broken—it’s just evolved over 30 years. The fundamentals are simple:
- pip install fetches and installs packages from PyPI
- Virtual environments keep projects isolated
- Lock files ensure reproducibility
- Modern tools like Poetry simplify the whole process
Next time you hit a dependency conflict, remember: you’re not fighting Python. You’re just seeing the natural result of 600,000+ packages trying to coexist peacefully.
At PythonSkillset, we’ve seen it all—from beginners running sudo pip install (please don’t) to pros who still forget to activate their virtual environment. The key is understanding what’s happening behind the curtain.
Now go build something awesome. And maybe add that lock file.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.