Senior 12 min · March 05, 2026

Python pip — Container ImportError from Global Packages

Container exited with ImportError: cannot import name 'Session' from 'requests' due to global packages.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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
✦ Definition~90s read
What is Python Packaging and pip?

pip is Python's de facto package installer, pulling dependencies from the Python Package Index (PyPI) — a repository hosting over 500,000 packages. It exists to solve the fundamental problem of distributing and installing third-party libraries, but it's a blunt instrument: by default, pip install drops packages into a global site-packages directory, shared across all Python projects on that system.

Imagine your kitchen has a spice rack.

This becomes a nightmare inside containers, where a Docker build might inherit host-level packages or accidentally install into the system Python, causing ImportError when your application can't find the right version — or finds the wrong one. The core issue is that pip has no built-in isolation; it's just a downloader and extractor, leaving dependency management entirely to you.

In the Python ecosystem, pip competes with Conda (for data science stacks) and Poetry (for modern dependency resolution), but pip remains the default because it ships with Python itself. You should not use pip alone for production applications — it lacks lock files, dependency resolution is naive (no SAT solver), and it will happily install conflicting versions.

Instead, pip is the engine you drive through requirements.txt or pyproject.toml, which pin exact versions and declare dependencies. The real power comes from combining pip with virtual environments (venv), which create isolated site-packages per project, or with Docker multi-stage builds that separate build-time from runtime dependencies.

When you hit a container ImportError at 3 AM, it's almost always because pip installed a package globally during the Docker build, and your application's Python interpreter can't see it — or sees a stale version from the base image. The fix is to never pip install without a virtual environment, always pin versions in requirements.txt (e.g., requests==2.31.0), and use --no-cache-dir to keep images lean.

For modern projects, pyproject.toml replaces setup.py and requirements.txt with a single declarative file that pip can read via pip install ., ensuring reproducible builds. And if you're publishing to PyPI, pip is the delivery mechanism — but you'll use tools like build and twine to create and upload wheels, not raw pip.

Plain-English First

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.

Global vs. Virtualenv Confusion
Running 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.
Production Insight
A team deployed a FastAPI app in a container based on 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.
The exact symptom: ImportError on a submodule that exists in one version but not the other, with no clear trace because both versions are present.
Rule of thumb: Always create and activate a virtual environment inside your container before pip install — it isolates your dependencies from the base image's global packages.
Key Takeaway
pip installs into whatever Python environment it's bound to — isolation is not automatic.
Global site-packages from base images can shadow or conflict with pip-installed packages, causing hard-to-debug ImportErrors.
Always use a virtual environment inside containers to guarantee dependency isolation and reproducibility.
Python pip Container ImportError Flow THECODEFORGE.IO Python pip Container ImportError Flow From global package leaks to modern dependency management Global Packages Leak pip installs system-wide, causing container ImportError Virtual Environment Isolates dependencies per project, non-negotiable requirements.txt Pins exact versions for reproducible builds pyproject.toml Modern standard for project metadata and deps Publish to PyPI Package distribution from local to public registry ⚠ pip vs pip3 vs pip2 confusion in containers Always use python -m pip to avoid version mismatch THECODEFORGE.IO
thecodeforge.io
Python pip Container ImportError Flow
Python Packaging Pip

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.

setup_virtual_environment.shPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# ── CREATING A VIRTUAL ENVIRONMENT ─────────────────────────────────────────
# The 'venv' module ships with Python 3.3+ — no separate install needed
# Always create inside your project folder so it's self-contained
python3 -m venv venv

# ── ACTIVATION ──────────────────────────────────────────────────────────────
# Mac/Linux: updates PATH to point to venv's Python and pip
source venv/bin/activate

# Windows Command Prompt:
# venv\Scripts\activate

# Windows PowerShell:
# venv\Scripts\Activate.ps1

# Your terminal prompt changes to show (venv) — visual confirmation you're isolated

# ── VERIFY YOU'RE IN THE RIGHT ENVIRONMENT ──────────────────────────────────
# Both should point INSIDE your project folder, not /usr/bin or /usr/local
which python
# Good: /home/user/myproject/venv/bin/python
# Bad:  /usr/bin/python3  (you're still global)

which pip
# Good: /home/user/myproject/venv/bin/pip
# Bad:  /usr/local/bin/pip  (wrong environment)

# ── INSTALL PACKAGES ────────────────────────────────────────────────────────
# Only lands inside this venv — completely invisible to other projects
pip install requests flask pytest

# Verify what's installed and where
pip list
# Shows ONLY packages in this venv, not global packages

# ── DEACTIVATION ────────────────────────────────────────────────────────────
deactivate
# PATH reverts. 'which python' now points to the system Python again.
# The packages you installed are still in venv/lib/ — they're just not active

# ── USING uv (faster alternative, Rust-based) ───────────────────────────────
# pip install uv  (or: curl -LsSf https://astral.sh/uv/install.sh | sh)
uv venv .venv          # creates a venv at .venv/ — significantly faster than python -m venv
source .venv/bin/activate
uv pip install requests  # 5-10x faster than pip for most packages
Output
(venv) user@machine:~/myproject$ which python
/home/user/myproject/venv/bin/python
(venv) user@machine:~/myproject$ which pip
/home/user/myproject/venv/bin/pip
(venv) user@machine:~/myproject$ pip install requests
Collecting requests
Downloading requests-2.31.0-py3-none-any.whl (62 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.6/62.6 kB 1.2 MB/s eta 0:00:00
Successfully installed certifi-2024.2.2 charset-normalizer-3.3.2 idna-3.6 requests-2.31.0
(venv) user@machine:~/myproject$ deactivate
user@machine:~/myproject$ which python
/usr/bin/python3 # back to global — requests is not available here
Watch Out: Never Commit Your venv/ Folder
Add 'venv/' and '.venv/' to your .gitignore the moment you create a project. The virtual environment folder contains compiled binary files that are platform-specific — a venv created on macOS will not work on Linux, and vice versa. It can easily reach 500MB for projects with heavy dependencies like PyTorch or TensorFlow. Share requirements.txt or pyproject.toml instead — both let any developer recreate the environment from scratch in under a minute.
Production Insight
Global pip installs cause version conflicts between projects on the same machine and are the root cause of most 'works on my machine' failures.
Docker containers isolate processes, not packages — you still need a virtual environment inside Docker to prevent base image package pollution from silently affecting your resolution.
Rule: activate a venv before touching pip. Make it the first thing you do when you open a terminal in a project directory. After a few weeks it becomes muscle memory.
Key Takeaway
A virtual environment is just a folder with an isolated Python binary and its own site-packages directory. Activating it only changes your PATH — nothing magical.
Create one per project, always — the cost is two commands and the benefit is never fighting dependency conflicts again.
In 2026, uv is a significantly faster alternative to pip + venv for the same workflow.

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.

manage_dependencies.shPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# ── APPROACH 1: pip freeze (simple, good for apps) ─────────────────────────
# Captures everything currently installed at exact versions
# Best for: application deployments where you need byte-for-byte reproducibility

pip freeze > requirements.txt
# Output includes direct AND transitive deps, all pinned:
# certifi==2024.2.2
# charset-normalizer==3.3.2
# idna==3.6
# requests==2.31.0

# ── APPROACH 2: Hand-written with ranges (good for libraries) ──────────────
# Only list your direct dependencies with reasonable version constraints
# Best for: libraries others will install alongside their own packages

cat > requirements.txt << 'EOF'
requests>=2.28,<3
flask>=3.0
pytest>=7.0
EOF

# ── APPROACH 3: pip-tools (best of both worlds, production standard) ────────
pip install pip-tools

# Create requirements.in — your direct dependencies with loose constraints
# This is the human-editable file you commit and maintain
cat > requirements.in << 'EOF'
requests>=2.28
flask>=3.0
pytest>=7.0
EOF

# pip-compile resolves the full dependency graph and generates requirements.txt
# --generate-hashes adds SHA256 hashes for supply chain security (optional but recommended)
pip-compile requirements.in
# or with hashes:
pip-compile --generate-hashes requirements.in

# pip-sync installs EXACTLY what's in requirements.txt AND removes anything not listed
# This guarantees your environment matches the lockfile exactly
pip-sync requirements.txt

# ── UPDATING DEPENDENCIES ───────────────────────────────────────────────────
# To update a specific package to a newer version:
pip-compile --upgrade-package requests requirements.in
# Updates requests and adjusts any transitive deps that change as a result

# To update everything to the latest compatible versions:
pip-compile --upgrade requirements.in

# ── CHECKING FOR SECURITY VULNERABILITIES ───────────────────────────────────
# pip-audit checks all installed packages against the Python Advisory Database
pip install pip-audit
pip-audit
# Reports any known CVEs in your installed dependencies
Output
# Contents of auto-generated requirements.txt after pip-compile:
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
# pip-compile requirements.in
#
blinker==1.7.0
# via flask
brotli==1.1.0
# via werkzeug
certifi==2024.2.2
# via requests
charset-normalizer==3.3.2
# via requests
click==8.1.7
# via flask
flask==3.0.2
# via -r requirements.in
idna==3.6
# via requests
itsdangerous==2.1.2
# via flask
jinja2==3.1.3
# via flask
markupsafe==2.1.5
# via jinja2
plugy==1.4.0
# via pytest
pytest==7.4.4
# via -r requirements.in
requests==2.31.0
# via -r requirements.in
werkzeug==3.0.1
# via flask
pip-sync complete. 14 packages installed, 0 packages removed.
Pro Tip: Use >= Not == for Direct Dependencies in Libraries
If you're building a library others will install, never pin your direct dependencies with '==' in your package definition. Use '>=' with an upper bound on the major version (e.g., 'requests>=2.28,<3'). Pinning with '==' means your library will conflict with any other package in your user's project that needs a different exact version of the same dependency — and they will not be able to install both, which means they cannot use your library at all.
Production Insight
pip freeze pins everything including transitive deps — exactly right for application deployments, wrong for libraries.
Libraries should use loose version ranges in pyproject.toml to avoid conflicting with users' other installed packages.
Rule: apps pin exactly (use pip freeze or pip-compile), libraries pin loosely (use ranges). Treating a library's dependency list like an application lockfile is the most common way to create ResolutionImpossible errors for your users.
Key Takeaway
pip freeze captures all installed packages including transitive deps — great for app deployments, but too restrictive for libraries you publish.
pip-tools (pip-compile) gives you both: a human-editable requirements.in and a machine-pinned requirements.txt with full transitive dependency tracking and comments.
Commit both requirements.in and requirements.txt — the diff between pip-compile runs 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.

pyproject.tomlPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# pyproject.toml — the single source of truth for your Python project
# Replaces setup.py, setup.cfg, and MANIFEST.in entirely

[build-system]
# Which tools are needed to BUILD this package
# pip downloads and uses these in an isolated temporary environment
# before building — no pre-installed tools required
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "weather-fetcher"              # The name users will 'pip install'
version = "1.0.0"                     # Follow semantic versioning: MAJOR.MINOR.PATCH
description = "Fetch weather data from public APIs with a clean interface"
readme = "README.md"                  # Displayed on PyPI project page
license = {file = "LICENSE"}
requires-python = ">=3.9"             # Minimum Python version your code supports
authors = [
    {name = "Your Name", email = "you@example.com"}
]
keywords = ["weather", "api", "cli"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]

# Direct dependencies — use ranges, never exact pins
# These are installed automatically when someone 'pip install weather-fetcher'
dependencies = [
    "requests>=2.28,<3",
    "python-dotenv>=1.0",
]

[project.urls]
Homepage = "https://github.com/yourname/weather-fetcher"
Documentation = "https://weather-fetcher.readthedocs.io"
Changelog = "https://github.com/yourname/weather-fetcher/blob/main/CHANGELOG.md"
Issues = "https://github.com/yourname/weather-fetcher/issues"

# Optional dependency groups — users install with: pip install weather-fetcher[dev]
# Keep dev tools out of the main dependency list
[project.optional-dependencies]
dev = [
    "pytest>=7.0",
    "pytest-cov>=4.0",
    "black>=24.0",
    "mypy>=1.0",
    "ruff>=0.3",
]
docs = [
    "sphinx>=7.0",
    "sphinx-rtd-theme>=2.0",
]

# Entry points: registers a 'weather' command in the user's terminal after install
# Python maps 'weather' to: call the main() function in weather_fetcher/cli.py
[project.scripts]
weather = "weather_fetcher.cli:main"
# After 'pip install .': user can type 'weather London' in any terminal

# ── Project layout that matches this pyproject.toml ─────────────────────────
# weather-fetcher/
# ├── pyproject.toml
# ├── README.md
# ├── LICENSE
# ├── CHANGELOG.md
# └── src/
#     └── weather_fetcher/
#         ├── __init__.py
#         └── cli.py

# The src/ layout is recommended — it prevents accidental imports from the
# project root during development, ensuring you're always testing the installed package

# ── Local development install ────────────────────────────────────────────────
# pip install -e .        (install core only)
# pip install -e .[dev]   (install core + dev tools)
# The -e flag means 'editable' — edit source files and changes are immediately
# reflected without reinstalling the package
Output
# Running: pip install -e .[dev]
Obtaining file:///home/user/projects/weather-fetcher
Installing build dependencies ... done
Checking if build backend supports build_editable ... done
Getting requirements to build editable ... done
Preparing editable install (pyproject.toml) ... done
Requirement already satisfied: requests>=2.28,<3 in ./venv/lib/python3.12/site-packages (2.31.0)
Collecting python-dotenv>=1.0
Downloading python_dotenv-1.0.1-py3-none-any.whl (19 kB)
Collecting pytest>=7.0
Downloading pytest-7.4.4-py3-none-any.whl (325 kB)
Collecting black>=24.0
Downloading black-24.3.0-cp312-cp312-linux_x86_64.whl (1.8 MB)
Collecting ruff>=0.3
Downloading ruff-0.3.4-py3-none-linux_x86_64.whl (8.0 MB)
Successfully installed black-24.3.0 python-dotenv-1.0.1 pytest-7.4.4 ruff-0.3.4 weather-fetcher-1.0.0
# Verifying the entry point works:
$ weather London
Fetching weather for London...
Temperature: 14°C, Partly cloudy
Interview Gold: Why pyproject.toml Replaced setup.py
setup.py was executable Python code, which meant pip had to run arbitrary code just to find out what a package's dependencies were — a security risk, a portability problem, and a reproducibility nightmare. pyproject.toml is static, declarative data. Build tools can read it without executing anything, making builds faster, safer, and more predictable. It also solves the bootstrapping problem: setup.py required setuptools to be pre-installed, while pyproject.toml's [build-system] table tells pip exactly what to install in an isolated build environment before attempting to build. This is the answer most interviewers are looking for when they ask about modern Python packaging.
Production Insight
setup.py required setuptools to be pre-installed — a chicken-and-egg problem that made tooling complicated and bootstrapping fragile.
pyproject.toml declares build system requirements so pip can bootstrap them in an isolated environment, with no pre-installed tools required.
Rule: migrate all projects to pyproject.toml. setup.py is a legacy format and should not be used for any new project started in 2024 or later.
Key Takeaway
pyproject.toml replaced setup.py because it is declarative static data rather than executable Python code — tooling can read it without running anything.
[build-system] declares what builds your package. [project] declares metadata and dependencies. [project.scripts] registers CLI entry points.
The src/ layout is the recommended project structure — it prevents accidental imports from the project root during testing.

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.

publish_to_pypi.shPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# ── PREREQUISITES ────────────────────────────────────────────────────────────
# Create accounts at:
#   https://test.pypi.org   (for testing)
#   https://pypi.org        (for production)
# Enable 2FA on both accounts — PyPI requires it for new publishers

pip install build twine

# ── STEP 1: BUILD ────────────────────────────────────────────────────────────
# Reads pyproject.toml and builds both wheel and source dist
# Creates files in the dist/ folder
python -m build
# Output:
#   dist/weather_fetcher-1.0.0-py3-none-any.whl   (wheel — fast install)
#   dist/weather_fetcher-1.0.0.tar.gz              (sdist — fallback)

# ── STEP 2: VERIFY before uploading ─────────────────────────────────────────
# Catches malformed README, missing metadata, and packaging mistakes
# before they become permanent PyPI entries
twine check dist/*
# Expected output: PASSED for both files

# ── STEP 3: TEST on TestPyPI — always do this first ─────────────────────────
twine upload --repository testpypi dist/*
# You'll be prompted for TestPyPI credentials or API token

# Verify the TestPyPI install works end-to-end:
pip install \
    --index-url https://test.pypi.org/simple/ \
    --extra-index-url https://pypi.org/simple/ \
    weather-fetcher
# --extra-index-url allows pip to find dependencies on real PyPI
# since test.pypi.org doesn't have all packages

# Test the entry point works:
weather London

# ── STEP 4: PUBLISH to real PyPI when satisfied ──────────────────────────────
twine upload dist/*
# After this, the package is live at https://pypi.org/project/weather-fetcher/
# Anyone can now: pip install weather-fetcher

# ── API TOKENS (strongly recommended over passwords) ─────────────────────────
# Go to pypi.org > Account Settings > API Tokens
# Create a token scoped to this specific project (not account-wide)
# Then pass it to twine:
export TWINE_USERNAME=__token__
export TWINE_PASSWORD=pypi-AgEIcHlwaS5vcmcA...  # your actual token
twine upload dist/*

# Or store in ~/.pypirc to avoid env vars:
# [pypi]
# username = __token__
# password = pypi-AgEIcHlwaS5vcmcA...

# ── AUTOMATING WITH GITHUB ACTIONS ──────────────────────────────────────────
# .github/workflows/publish.yml
# Triggers on: push of tags matching v*.*.* (e.g., v1.0.0)
# Steps: checkout → build → test on TestPyPI → publish to PyPI
# Use PyPI's Trusted Publishers (OIDC) to skip token management entirely
# See: https://docs.pypi.org/trusted-publishers/
Output
# python -m build output:
* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
- hatchling
* Getting build dependencies for sdist...
* Building sdist...
* Building wheel from sdist
* Creating isolated environment: venv+pip...
* Building wheel...
Successfully built weather_fetcher-1.0.0.tar.gz and weather_fetcher-1.0.0-py3-none-any.whl
# twine check dist/* output:
Checking dist/weather_fetcher-1.0.0-py3-none-any.whl: PASSED
Checking dist/weather_fetcher-1.0.0.tar.gz: PASSED
# twine upload dist/* output (real PyPI):
Uploading distributions to https://upload.pypi.org/legacy/
Uploading weather_fetcher-1.0.0-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 14.2 kB / 14.2 kB
Uploading weather_fetcher-1.0.0.tar.gz
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9.1 kB / 9.1 kB
View at: https://pypi.org/project/weather-fetcher/1.0.0/
Pro Tip: Use Trusted Publishers to Eliminate PyPI Token Management
PyPI's Trusted Publishers feature uses GitHub Actions' OIDC integration to authenticate your publishing workflow without storing any secrets. You configure a trusted publisher on PyPI (specifying your GitHub repo, workflow file, and environment), and your GitHub Actions workflow can publish without needing a TWINE_PASSWORD or any stored API token. This is more secure than token-based auth because there are no long-lived credentials that can leak — the authentication is ephemeral and tied to a specific workflow run.
Production Insight
PyPI never allows overwriting a published version — a bad release lives there permanently. Plan for 1.0.1 as your correction path, not version deletion.
Always run twine check before uploading — it catches a surprising number of metadata problems that would make your PyPI project page look broken.
Rule: always test the full end-to-end install from TestPyPI before touching real PyPI. The extra ten minutes has saved more releases than any other practice.
Key Takeaway
PyPI publishing is python -m build then twine upload — two commands, but the one-time account and token setup matters.
Always publish both wheel (.whl) and source dist (.tar.gz) — wheel for the 99% case, source distribution for unusual platforms.
You cannot delete or overwrite a published version on real PyPI — always verify on TestPyPI first, and use twine check before every upload.

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.

SymlinkAmbush.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — python tutorial

// Don't let this be your Friday night
$ which pip
/usr/bin/pip
$ pip --version
pip 20.0.2 from /usr/lib/python2.7/dist-packages/pip (python 2.7)

// What you wanted:
$ python3 -m pip --version
pip 21.3.1 from /usr/lib/python3.8/dist-packages/pip (python 3.8)

// Safe pattern for scripts and CI:
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
Output
pip 20.0.2 from /usr/lib/python2.7/dist-packages/pip (python 2.7) ← BAD
pip 21.3.1 from /usr/lib/python3.8/dist-packages/pip (python 3.8) ← GOOD
Production Trap:
If your Dockerfile has 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.
Key Takeaway
Always use 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.

GitInstallPinning.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — python tutorial

// DON'T — floating dependency
pip install git+https://github.com/yourorg/internal-auth.git

// DO — pin to commit hash
pip install git+https://github.com/yourorg/internal-auth.git@a1b2c3d4e5f6

// DO — pin to tag
pip install git+https://github.com/yourorg/internal-auth.git@v1.2.3

// requirements.txt format:
git+https://github.com/yourorg/internal-auth.git@a1b2c3d4e5f6#egg=internal-auth

// CI-safe install:
python -m pip install --no-cache-dir -r requirements.txt
Output
Successfully installed internal-auth-0.1.0 (from git commit a1b2c3d)
Senior Shortcut:
If you control the repo, push a tag for every release and put the tag in requirements.txt. If it's a third-party repo with no tags, fork it and tag your fork. Floating deps from GitHub are the #1 cause of 'works on my machine' in 2024.
Key Takeaway
Always pin GitHub installs to a commit hash or tag. Treat git-based dependencies like untrusted third-party code.

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.

uv_vs_poetry_benchmark.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — python tutorial

import subprocess
import time

# Simulate installing 'requests' + 'flask' in a fresh venv
# UV install
start = time.time()
subprocess.run(["uv", "pip", "install", "requests", "flask", "-q"], capture_output=True)
uv_time = time.time() - start

# Poetry install (requires poetry.lock generation)
subprocess.run(["poetry", "init", "-n", "--no-interaction"], capture_output=True)
start = time.time()
subprocess.run(["poetry", "add", "requests", "flask", "-q"], capture_output=True)
poetry_time = time.time() - start

print(f"UV: {uv_time:.2f}s")
print(f"Poetry: {poetry_time:.2f}s")
Output
UV: 1.24s
Poetry: 12.87s
Production Trap:
Don't swap Poetry for UV mid-project without verifying your custom build hooks. UV's Rust resolver is strict — it will reject malformed version specifiers Poetry silently accepted.
Key Takeaway
UV is the fastest dependency resolver in Python right now. Poetry is legacy you inherit. For greenfield, write UV.

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.

switch_to_uv_in_dockerfile.shPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — python tutorial

# Before (slow pip hell)
# FROM python:3.11-slim
# COPY requirements.txt .
# RUN pip install --no-cache-dir -r requirements.txt

# After (UV speed)
FROM python:3.11-slim
COPY requirements.txt .
RUN pip install uv && \
    uv pip install --system --no-cache -r requirements.txt

# Verify no pip leakage
RUN python -c "import requests; print('Deploy ready')"
Output
Deploy ready
Senior Shortcut:
Add --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.
Key Takeaway
Drop-in replacement: 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.

uv_vs_conda.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
// io.thecodeforge — python tutorial

# Conda: full ecosystem, but slow
# conda create -n myenv python=3.11 numpy pandas
# conda activate myenv

# UV: Python-only, lightning fast
# uv venv myenv --python 3.11
# uv pip install numpy pandas

# UV resolves in seconds, Conda in minutes
# Use Conda only for non-Python deps
Output
Environment created in 0.3s vs 45.2s
Production Trap:
If you use Conda with pip inside, you get the worst of both worlds — Conda's slowness plus pip's flat namespace. Pick one resolver.
Key Takeaway
Use Conda only when you need non-Python binaries. Use UV for pure Python speed.

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.

uv_advantages.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
// io.thecodeforge — python tutorial

# Poetry: Python-dependent, slow, lockfile inflexible
# pip install poetry && poetry install  # 10s+ bootstrapping

# Conda: cross-platform, but 2-5x slower resolution
# conda env create -f environment.yml

# UV: zero bootstrap, pip-compatible, instant
# curl -LsSf https://astral.sh/uv/install.sh | sh
# uv sync  # uses existing pyproject.toml
Output
Resolved 42 packages in 0.18s
Production Trap:
Poetry's lockfile is Poetry-only. UV uses standard PEP 751 or pip's requirements.txt — no vendor lock-in.
Key Takeaway
UV wins because it's pip-native, Rust-fast, and installs itself without Python.
● Production incidentPOST-MORTEMseverity: high

Production Deployment Failed Due to Global pip Install Conflict

Symptom
Container exited with ImportError: cannot import name 'Session' from 'requests'. The Dockerfile ran pip install -r requirements.txt without a virtual environment, so it picked up the globally cached requests==2.25 instead of the required requests==2.31. The error only appeared at runtime — the build step completed successfully and appeared clean.
Assumption
The team assumed Docker containers are isolated enough that virtual environments are unnecessary. They had read that Docker provides process isolation and concluded that package isolation was included. They installed packages directly into the system Python inside the container for simplicity, reasoning that fewer steps meant fewer things to go wrong.
Root cause
The base Docker image (python:3.11-slim) had pre-installed packages from a previous layer cache. When pip install ran without a venv, it resolved against the cached global packages, silently downgrading requests from 2.31 to 2.25 to satisfy a transitive dependency conflict introduced by another package in the base image. The downgrade happened silently — pip did not raise an error or warning, it simply chose the version that satisfied all constraints including those from packages the team didn't know were pre-installed in the base image.
Fix
Added 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.
Key lesson
  • 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
Production debug guideWhen pip install fails or your project breaks after installing a package4 entries
Symptom · 01
ImportError or ModuleNotFoundError after pip install appeared to succeed
Fix
Check which Python and pip are actually active: run 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.
Symptom · 02
pip install succeeds but the installed package version is not what you requested
Fix
Run 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.
Symptom · 03
ResolutionImpossible error during pip install -r requirements.txt
Fix
Two or more packages have version constraints that cannot be satisfied simultaneously. Run 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.
Symptom · 04
Package works locally but fails in CI or Docker
Fix
Run 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.
★ Python Packaging Debugging Cheat SheetWhen pip and virtual environments misbehave — immediate diagnostic steps
ModuleNotFoundError after installing a package
Immediate action
Verify you are in the correct virtual environment — this is the cause 90% of the time
Commands
which python && which pip
pip list | grep <missing_package>
Fix now
If which python does not point inside your project folder, activate the venv: 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.
Dependency conflict — ResolutionImpossible error+
Immediate action
Identify which packages are pulling in incompatible versions of the same dependency
Commands
pip check
pip install --dry-run -r requirements.txt 2>&1 | grep -i conflict
Fix now
Relax the version constraint on the conflicting package, or use pip-compile to resolve the dependency graph automatically. pip-compile's output shows which of your direct dependencies caused each transitive pin.
Old package version installed despite requesting a newer one+
Immediate action
Check if another dependency is pinning it to an older version as a transitive constraint
Commands
pip show <package> # shows the Required-by field
pip install <package>==<desired_version> --dry-run # shows what would change
Fix now
Identify which package in Required-by is constraining the version. Either update that package, relax your requirements, or use pip-compile to find a consistent resolution across all constraints.
requirements.txt + pip vs pyproject.toml + Build Tools
Aspectrequirements.txt + pippyproject.toml + build tools
Primary use caseInstalling app dependencies reproducibly across machinesDefining package metadata, dependencies, and publishing to PyPI
FormatPlain text, one package per line, optional version specifiersTOML structured config file — machine-readable, human-editable
Version pinningExact pins via pip freeze for lockfiles; loose ranges for flexibilityLoose ranges recommended in [project.dependencies]; lockfiles via pip-compile
Build system declarationNot applicable — not a package definitionRequired via [build-system] table — tells pip how to build before building
CLI entry pointsNot supportedDefined via [project.scripts] — installs terminal commands alongside the package
Optional dependency groupsRequires separate files (requirements-dev.txt, requirements-test.txt)Built-in via [project.optional-dependencies] — pip install package[dev]
Transitive dependency trackingManual (pip freeze) or automated via pip-compile with annotationsHandled by lockfile tools (pip-compile, Poetry's poetry.lock, uv.lock)
Editable installsNot applicablepip install -e . supported natively — changes to source reflect immediately
Who reads itpip at install timepip, build, hatch, poetry, flit, uv, and any PEP 517-compliant tool
ReplacesNothing — it is the standard for dependency installation manifestssetup.py, setup.cfg, and MANIFEST.in — all legacy formats

Key takeaways

1
A virtual environment is just a folder with an isolated Python binary and its own site-packages directory. Activating it changes your PATH
nothing more magical than that. Create one per project, every time, with no exceptions.
2
pip freeze captures all installed packages including transitive deps at exact versions
perfect for application deployments that need byte-for-byte reproducibility. For libraries you publish to PyPI, use loose version ranges in pyproject.toml instead, or you will create ResolutionImpossible errors for your users.
3
pyproject.toml replaced setup.py because it is declarative static data rather than executable Python code
pip can read your build requirements and metadata without running arbitrary code from the internet, making the process faster and safer.
4
Always test a PyPI release on test.pypi.org before publishing to the real PyPI
you cannot delete or overwrite a version once published, ever. A bad release is permanent.
5
pip-compile from pip-tools gives you deterministic builds
a human-editable requirements.in you maintain and a machine-generated requirements.txt with full transitive dependency resolution and annotations. Commit both to version control.
6
Editable installs (pip install -e .) let you develop a package locally without reinstalling after every change
source edits are immediately reflected because pip creates a link to your source folder rather than copying files into site-packages.

Common mistakes to avoid

6 patterns
×

Installing packages globally instead of in a virtual environment

Symptom
ImportError or wrong package version on a different machine or in CI. 'which python' points to /usr/bin/python3 or /usr/local/bin/python3 instead of a path inside your project folder. Two projects on the same machine interfere with each other's dependencies.
Fix
Always run 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)

Symptom
Users report ResolutionImpossible errors when trying to install your library alongside their other packages. Your pinned transitive dependencies conflict with versions they need for their project.
Fix
For libraries, only list direct dependencies in pyproject.toml with loose version ranges (e.g., 'requests>=2.28,<3'). Reserve exact pinning for application deployments. If you publish a library with '==' pins on transitive dependencies, you will break many of your users' projects.
×

Forgetting to bump the version before publishing to PyPI

Symptom
twine upload fails with '400 File already exists'. PyPI permanently rejects any upload with a filename that matches an existing release — you cannot overwrite version 1.0.0, ever, even with identical content.
Fix
Update the version field in pyproject.toml before every release. Follow semantic versioning: bump PATCH for bug fixes, MINOR for new backwards-compatible features, MAJOR for breaking changes. Use hatch version patch or bump2version to automate version bumping and avoid manual errors.
×

Committing the venv/ or .venv/ folder to Git

Symptom
Repository size balloons by hundreds of megabytes. Build failures on different operating systems because venv binaries are platform-specific. Teammates get mysterious merge conflicts on binary files deep inside the venv folder.
Fix
Add both 'venv/' and '.venv/' to .gitignore before the first commit of any project. Share requirements.txt or pyproject.toml instead. Document the setup process in README.md so any developer can recreate the environment with one or two commands.
×

Not using pip-compile for deterministic builds in CI

Symptom
CI builds produce different results on different days because pip resolves transitive dependencies freshly each time and a package somewhere in the graph released a new version. A build that passed on Monday fails on Wednesday with no code changes.
Fix
Use pip-compile from pip-tools: write requirements.in with loose direct dependencies, run pip-compile to generate a fully-pinned requirements.txt with transitive annotation comments. Commit both files to version control. CI installs from the pinned file, ensuring identical environments across all runs.
×

Skipping TestPyPI and publishing directly to the real PyPI

Symptom
A release with a broken README, missing file, wrong version, or bad metadata goes live permanently. You cannot delete it or overwrite it — you can only publish a new version with the fix, and the broken version remains visible forever.
Fix
Always run 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 PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What's the difference between a wheel (.whl) and a source distribution (...
Q02JUNIOR
If two of your Python projects need different versions of the same libra...
Q03SENIOR
Why was setup.py considered a security and reproducibility problem, and ...
Q04SENIOR
What is the difference between pip freeze and pip-compile, and when woul...
Q05SENIOR
How would you set up a CI/CD pipeline to publish a Python package to PyP...
Q01 of 05JUNIOR

What's the difference between a wheel (.whl) and a source distribution (.tar.gz), and why does PyPI recommend publishing both?

ANSWER
A wheel (.whl) is a pre-built binary package that installs instantly — pip extracts it directly into site-packages with no compilation step. A source distribution (.tar.gz) contains raw source code that gets built on the user's machine during installation using the build backend declared in pyproject.toml. PyPI recommends publishing both because the wheel handles the vast majority of installations instantly on common platforms, while the source distribution serves as a fallback for unusual platforms or CPU architectures where no matching pre-built wheel exists. For packages with C extensions like numpy, this distinction is especially important — building from source takes minutes and requires a compiler, while installing a wheel is instant.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between pip install and pip install -e?
02
Should I commit my virtual environment folder to Git?
03
What is the difference between install_requires in setup.py and dependencies in pyproject.toml?
04
What is a wheel (.whl) file and why is it faster than installing from source?
05
How do optional dependency groups work in pyproject.toml?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's Advanced Python. Mark it forged?

12 min read · try the examples if you haven't

Previous
Virtual Environments in Python
9 / 17 · Advanced Python
Next
GIL — Global Interpreter Lock