Python pip — Container ImportError from Global Packages
Container exited with ImportError: cannot import name 'Session' from 'requests' due to global packages.
20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.
- Virtual environments isolate each project's dependencies in a private site-packages directory — create one per project, no exceptions
- pip install fetches packages into the active environment — always activate a venv first or you're installing into the global Python and asking for trouble
- requirements.txt pins dependencies for reproducibility; pip freeze captures everything including transitive deps, which is right for apps but wrong for libraries
- pyproject.toml is the modern standard replacing setup.py — it's declarative static data, not executable code, which makes it faster and safer for tooling to parse
- Always test on TestPyPI before publishing to real PyPI — you cannot overwrite or delete a published version, ever
- Use >= ranges for library dependencies, exact pins for application deployments — mixing these up causes dependency conflicts for your users
Imagine your kitchen has a spice rack. Every dish you cook needs different spices — some recipes need chili, others need cinnamon. If you threw every spice into one giant pile, recipes would clash and ruin each other. Python packaging is your way of giving each project its own private spice rack. pip is the delivery service that fetches exactly the spices you need, and a virtual environment is the isolated kitchen counter where only that project's spices live. The requirements.txt is your shopping list — hand it to pip and it fetches exactly what you need. pyproject.toml is the recipe card for your dish itself, the one you hand to other chefs so they can recreate it exactly.
Every Python project you build beyond a single script will eventually depend on code someone else wrote — a web framework, a data parser, a testing library. The moment you install that code, you're participating in Python's packaging ecosystem, whether you realize it or not. Get it wrong and you end up with the classic 'it works on my machine' problem, broken deployments, and version conflicts that take hours to debug and are genuinely difficult to explain to a product manager.
The good news is that Python's packaging ecosystem has matured significantly. The chaos of setup.py, setup.cfg, and MANIFEST.in is giving way to pyproject.toml. pip has better dependency resolution than it did even a few years ago. Virtual environments are a two-command operation. The tools have caught up to the problems they were designed to solve.
This article covers the four things you actually need to know: how virtual environments work and why they're non-negotiable, how to manage dependencies reproducibly with requirements.txt and pip-compile, how to structure a project with pyproject.toml, and how to publish a package to PyPI without making an irreversible mistake. Every section comes from real production experience — I'll tell you what trips people up and why, not just what the commands are.
Why Python pip Can Leak Global Packages Into Your Container
Python pip is the default package installer for Python, pulling libraries from the Python Package Index (PyPI) and placing them into site-packages directories. Its core mechanic is dependency resolution: it reads a requirements file or CLI arguments, resolves version constraints, downloads wheels or source distributions, and installs them into a target environment. The critical detail is that pip installs into whatever Python interpreter it's bound to — system Python, a virtual environment, or a container image layer — and it does not isolate packages from the global site-packages unless explicitly told to.
In practice, pip's behavior depends on the environment's Python path. When you run pip install inside a Docker container without first creating a virtual environment, packages land in /usr/local/lib/python3.x/site-packages. If your container image inherits a base image that already has packages there (e.g., from apt-installed python3-requests), pip's installation can silently shadow or conflict with those global packages. The result: an ImportError that only appears in production because the container's global site-packages differ from your local virtualenv. Pip has no built-in guard against this — it's your responsibility to control the target.
Use pip with virtual environments (or pip install --target) whenever you need reproducible, isolated dependencies. In containerized deployments, always create a fresh virtual environment inside the image and activate it before installing. This prevents the 'works on my machine' class of bugs where a global package version mismatch causes runtime failures. Pip is a tool, not a policy — the isolation is up to you.
pip install outside a virtualenv modifies the system Python's site-packages — this is often unintentional and can break OS-level tools that depend on specific package versions.python:3.11-slim which pre-installs urllib3 via apt. Their requirements.txt pinned urllib3<2. Pip installed the older version, but the apt version remained in the same site-packages directory, causing a ModuleNotFoundError: No module named 'urllib3.util.ssl_' at startup.ImportError on a submodule that exists in one version but not the other, with no clear trace because both versions are present.Why Virtual Environments Are Non-Negotiable
When you install a package globally with pip, it lands in your system Python installation. That sounds fine until Project A needs requests==2.28 and Project B needs requests==2.31. Python can only hold one version of a package at a time in a given Python installation, so one of your projects will always be broken. Virtual environments solve this by creating a completely isolated Python interpreter and its own package directory for each project. Every project gets its own private spice rack, and they never interfere with each other.
Creating one is a single command, but understanding what it actually does changes how you work. A virtual environment is just a folder containing a Python binary, a pip binary, and a site-packages directory. When you activate it, your shell's PATH is temporarily updated so that typing 'python' points to that isolated binary instead of your system one. When you deactivate, the PATH change is reversed. Nothing magical — just smart path management. The packages you installed in the venv are still there in the folder; they're just not on your active PATH anymore.
This also means you can have as many virtual environments as you want without them interfering with each other or with your system Python. Some developers create one per project (the most common approach), some create per-branch environments for complex multi-branch work. The cost is two commands and a few hundred megabytes of disk. The cost of not using them is dependency hell, broken deployments, and the specific kind of frustration that comes from code that works on your machine and nowhere else.
In 2026, you also have the option of uv — a Rust-based package manager and virtual environment tool from Astral — which creates environments and installs packages significantly faster than the traditional pip + venv combination. Whether you use python -m venv or uv venv, the conceptual model is identical.
Pinning Dependencies with requirements.txt — The Right Way
A requirements.txt file is a plain-text manifest of every package your project needs to run. pip reads it line by line and installs everything specified. This is what makes your project reproducible across different machines, CI pipelines, teammates' laptops, and production servers — anyone who runs pip install -r requirements.txt gets an environment that behaves identically to yours.
There is a critical distinction that trips up most developers early on: direct dependencies versus transitive dependencies. Direct dependencies are packages you explicitly import in your code — requests, flask, pytest. Transitive dependencies are packages that your dependencies depend on — certifi, urllib3, click. When you run pip freeze, you get every single installed package at exact versions, including all the transitive ones. This is complete and deterministic, which is exactly what you want for application deployments. But it's too restrictive for libraries you publish for others to use, because pinning transitive dependencies to exact versions will almost certainly conflict with your users' other packages.
The professional workflow that solves both problems: create a requirements.in with loose version constraints for your direct dependencies only, then use pip-compile from the pip-tools package to resolve the full dependency graph and generate a deterministic requirements.txt with every transitive dependency pinned and annotated with which direct dependency caused it. This gives you a human-editable source of intent (requirements.in) and a machine-generated lockfile (requirements.txt). Commit both. CI installs from the pinned file. When you need to update dependencies, you edit requirements.in and re-run pip-compile — the diff shows you exactly what changed and why.
pyproject.toml — The Modern Way to Define a Python Package
For years, Python packaging was a mess of setup.py, setup.cfg, and MANIFEST.in files with inconsistent behaviour and no clear standard. PEP 517 and PEP 518 introduced pyproject.toml as the single unified configuration file for Python projects. Every modern project — whether you're publishing to PyPI or just organising an internal monorepo — should use it. If you're still using setup.py for a new project in 2026, you're taking on unnecessary legacy complexity.
The key insight that makes pyproject.toml important is what it changes architecturally: it separates 'build system requirements' (the tools needed to build your package) from 'project metadata' (what your package actually is). This lets pip know how to build your package before it has even installed your build tool. Previously, setup.py required setuptools to already be installed — a genuine chicken-and-egg problem. With pyproject.toml, pip reads the [build-system] table, creates an isolated build environment, installs the build tools declared there, and then builds. No pre-installed tools required.
The [project] table is where you declare your package name, version, dependencies, and entry points. Entry points are particularly useful in practice — they let you define command-line scripts that get installed alongside your package, so users can run your-tool directly in their terminal rather than typing python -m your_module.cli every time. If you've ever installed a package and gained a new CLI command, that's an entry point.
For build backends, the main options in 2026 are hatchling (part of Hatch, modern and fast), setuptools (the legacy standard, still widely used), flit (minimal, good for pure Python packages), and maturin (for Rust extensions). The choice of backend doesn't affect your package's users — they never see it. It only affects how your package gets built during python -m build.
Publishing Your Package to PyPI — From Local to the World
Publishing to PyPI (Python Package Index) feels intimidating until you understand it's just two commands after a one-time setup. PyPI is the repository pip queries by default whenever you run pip install. Publishing there means anyone in the world can install your package with pip install your-package-name — no other configuration needed.
The build process produces two distribution formats: a wheel (.whl) — a pre-built binary that installs in milliseconds — and a source distribution (.tar.gz) that gets built on the user's machine. The wheel is the fast path that handles the vast majority of installations. The source distribution is the fallback for unusual platforms or architectures where no pre-built wheel is available. Always publish both.
Before publishing to the real PyPI, test on TestPyPI first. It lives at test.pypi.org, behaves identically to the real PyPI, and is completely separate — packages published there don't appear on the real PyPI and vice versa. You can upload, install, and verify the complete experience without any risk. This step is not optional: you cannot overwrite or delete a published version on the real PyPI. If you publish 1.0.0 with a broken README or a missing file, that version is permanently broken and you'll need to publish 1.0.1 to fix it.
For production-quality publishing workflows, use a GitHub Actions pipeline that triggers on version tag pushes. Store your PyPI API token as a GitHub Secret, use Trusted Publishers (PyPI's OIDC integration) to avoid token management entirely, and build in a clean environment every time. This removes the 'I published from my half-configured dev machine' class of problems entirely.
pip vs pip3 vs pip2 — Why Your Container Just Broke at 3 AM
You type pip install requests and it works on your laptop. Your CI pipeline uses pip3 and also works. Then your colleague runs the same command on a legacy image and gets a wall of red. Welcome to Python's packaging identity crisis.
Python 2 is dead. But its ghosts live in enterprise Docker images pinned to Ubuntu 16.04. The binary pip might point to Python 2.7 or Python 3 depending on how Python was installed. pip3 always targets Python 3. pip2 targets Python 2 — and shouldn't exist in any image you deploy today.
The trap: inside a virtual environment, pip is symlinked to the Python version that created the venv. Outside a venv, you're at the mercy of your distro's symlink game. Alpine images ship pip for Python 3. Debian Buster defaults pip to Python 2 if both are installed.
Never rely on pip alone. Always verify with pip --version. Or better: standardise on python -m pip for every invocation. That guarantees you're using the pip associated with the Python interpreter you think you're using. No symlink roulette.
RUN pip install outside a venv and the base image ships both Python 2 and 3, you are installing into Python 2 silently. Pin your base image version and always use python -m pip in your Dockerfiles.python -m pip instead of bare pip to avoid Python version mismatch.Installing Packages from GitHub — The Dependency Hell You Code Yourself Into
Your startup's internal library isn't on PyPI. It lives on a private GitHub repo. Someone says 'just pip install from git'. Two months later, you can't reproduce the build and your CTO is asking why staging is broken.
pip can install directly from git URLs. That's not the problem. The problem is that pip install git+https://... pulls the latest commit from the default branch unless you pin a refspec. No tag? No commit hash? You just introduced a floating dependency into your production pipeline. Every deployment becomes a Schrödinger's build.
The fix is brutal but simple: always pin to a commit hash or a tag. If the repo doesn't tag releases, you need to have a hard conversation with the maintainers. Also: never put credentials in the URL. Use SSH keys or your CI's deploy token.
Another footgun: pip caches git clones in ~/.cache/pip. If your CI runner shares a home directory across builds (don't), you'll get stale clones. Always do pip install --no-cache-dir git+... in CI to force a fresh clone.
UV vs. Poetry — Why Your Next Project Shouldn't Start with Poetry
Poetry was the savior from pip’s dependency hell. Now it’s the bottleneck. UV is a Rust-based drop-in replacement that installs packages 10-100x faster and resolves dependencies without melting your laptop. Poetry still works. UV makes you forget what pip install ever felt like.
The core difference: Poetry treats your project as a package by default. UV treats speed and correctness as the default. UV supports PEP 517/518 builds natively, has zero config for monorepos, and doesn’t force a pyproject.toml lock-in dance. Poetry’s lock file is fine. UV’s resolver is nuclear. If you’re starting a new project today and you aren’t running on a Raspberry Pi from 2015, pick UV.
Production reality: UV integrates with existing requirements.txt and pyproject.toml files. Poetry doesn’t play well with others. UV is what Poetry would be if it had been rewritten by people who debug production outages.
Can I Use UV With Existing pip requirements.txt Files? — Yes, And It's Stupid Easy
Short answer: yes. Long answer: you should have switched yesterday. UV reads requirements.txt as a native format. No conversion, no uv init ceremony. If your CI/CD pipeline is still crying over pip install -r requirements.txt taking 90 seconds, drop in UV and watch it finish in 4.
Here’s how it works: replace pip install -r requirements.txt with uv pip install -r requirements.txt. That’s it. UV respects the same --no-cache, --target, and --constraint flags. Your Dockerfile? Change one line. Your Makefile? One grep. Your soul? Restored.
Why this matters in production: UV’s resolver is parallelized and stateless. It doesn’t re-download wheels it already verified. If your requirements.txt has torch, tensorflow, or any binary-heavy package, UV will save you 3-5 minutes per build. That’s not nice. That’s do-or-die for deploy latency.
--system flag to uv pip install if you want to bypass venv in containers. Otherwise, UV defaults to insisting on a virtual environment — a good habit you can break deliberately.uv pip install -r requirements.txt works exactly like pip, only faster. No migration pain, just time saved.UV vs. Conda — Why Your Python Environment Manager Might Be the Bottleneck
Conda is a full ecosystem manager — it handles Python, R, C libraries, and binary packages. That breadth comes with a cost: slow dependency resolution and bloated environments. UV, built in Rust, focuses exclusively on Python packages. It resolves dependencies 10–100x faster than Conda. Conda excels when you need non-Python binaries like CUDA drivers or OpenCV with system-level dependencies. UV dominates when you control your Python stack — CI pipelines, Docker images, or development environments. The tradeoff is clear: Conda for cross-language chaos, UV for Python purity. If your project doesn't need Conda's compilers or channels, UV removes the slowness without sacrificing lockfile guarantees. Switching from Conda to UV cuts environment setup from minutes to seconds. Your container builds will thank you.
What UV Gives You That Poetry and Conda Don't — Raw Speed and Zero Magic
Poetry adds lockfile management and dependency resolution with a promise of determinism, but hides too much behind magic — you fight it when you need pip-compatibility or custom index URLs. Conda solves cross-language dependencies but is painfully slow and forces you into its channel ecosystem. UV breaks both bottlenecks. It's pip-compatible out of the box — your existing requirements.txt and pyproject.toml work without changes. Resolution happens in milliseconds, not minutes. UV also avoids Poetry's lockfile formatting battles and Conda's environment sprawl. The killer advantage: UV is a single binary with no Python runtime dependency to install itself. Download it, run it. No more 'pip install poetry' bootstrapping or Conda init scripts. For teams shipping containers or CI workflows, UV removes the friction Poetry and Conda introduce.
Production Deployment Failed Due to Global pip Install Conflict
python3 -m venv /opt/venv and ENV PATH=/opt/venv/bin:$PATH to the Dockerfile before any pip install command. Used --no-cache-dir flag on pip install to avoid layer cache pollution. Added pip list --format=freeze > installed.txt as a post-install build step for audit logging — this file is archived as a build artifact so the exact installed state can be inspected for any past deployment. Switched to pip-compile from pip-tools for deterministic dependency resolution going forward.- Always use a virtual environment even inside Docker containers — Docker provides process isolation, not package isolation. They solve different problems.
- Docker layer caching can silently pollute dependency resolution by carrying forward packages from base images that pip will factor into its resolution algorithm
- Pin transitive dependencies with pip-compile, not just direct ones — a requirements.txt that only lists your direct dependencies is not a lockfile
- Audit installed packages in CI with pip list --format=freeze and archive the output as a build artifact so you can diagnose exactly what was installed in any past deployment
which python and which pip. If they don't point inside your venv/ directory, you installed to the wrong environment. The package is in a different Python's site-packages, not the one your script is running under. Activate the venv and reinstall.pip list | grep <package> to confirm the installed version. Run pip show <package> to see what required it. Another dependency almost certainly pulled in a conflicting version as a transitive dependency. Use pip check to detect all dependency conflicts in the current environment at once.pip install --dry-run -r requirements.txt to see the full conflict trace. Identify which packages are pulling in incompatible versions of the same dependency. Relax your version constraints or use pip-compile to find a compatible set automatically — pip-compile's output includes comments showing which package requires each transitive dependency.pip freeze in both environments and compare the outputs line by line. Look for version differences on any package, not just the one throwing the error — the failing package may be a transitive dependency of something that differs. Also check Python version: python --version must match across environments. If requirements.txt was generated with pip freeze locally, it may include platform-specific packages that don't exist on the CI platform.which python && which pippip list | grep <missing_package>source venv/bin/activate, then reinstall: pip install <package>. If the package appears in pip list but Python still can't import it, you have multiple Python installations and are running the wrong one.Key takeaways
Common mistakes to avoid
6 patternsInstalling packages globally instead of in a virtual environment
python3 -m venv venv && source venv/bin/activate before touching pip in any project. Verify the active environment with which python — it must point inside your project folder. Consider adding a check to your project's Makefile or scripts that warns if no venv is active.Using pip freeze output as the dependency list for a library (not an application)
Forgetting to bump the version before publishing to PyPI
hatch version patch or bump2version to automate version bumping and avoid manual errors.Committing the venv/ or .venv/ folder to Git
Not using pip-compile for deterministic builds in CI
Skipping TestPyPI and publishing directly to the real PyPI
twine upload --repository testpypi dist/* and then pip install --index-url https://test.pypi.org/simple/ your-package before touching the real PyPI. The entire end-to-end test takes under five minutes and has caught more bad releases than any other practice.Interview Questions on This Topic
What's the difference between a wheel (.whl) and a source distribution (.tar.gz), and why does PyPI recommend publishing both?
Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.
That's Advanced Python. Mark it forged?
12 min read · try the examples if you haven't