Python pip — Container ImportError from Global Packages
Container exited with ImportError: cannot import name 'Session' from 'requests' due to global packages.
- 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 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.
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.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
That's Advanced Python. Mark it forged?
6 min read · try the examples if you haven't