pip and venv, at work
Twenty-eight lessons on Python packaging, one concept per page: pip itself and PyPI; what pip install resolves and the four sources it installs from; the venv module, what activation changes in PATH, one script per shell, and deactivate; the EXTERNALLY-MANAGED marker, the PEP 668 refusal, the break-system-packages bypass and virtualenv beside the stdlib's venv; the pin set of pip list, pip show, pip freeze and requirements.txt replayed into a second environment; wheels, sdists and the build backend, pip download, editable installs; the pip cache, offline installs, upgrading pip itself and pip uninstall; then pyproject.toml, pip check, publishing to PyPI, and the wider field.
One concept, one page, one real capture. That's the rhythm.
pip and venv, at work
Twenty-eight lessons on Python packaging, one concept per page: pip itself and PyPI; what pip install resolves and the four sources it installs from; the venv module, what activation changes in PATH, one script per shell, and deactivate; the EXTERNALLY-MANAGED marker, the PEP 668 refusal, the break-system-packages bypass and virtualenv beside the stdlib's venv; the pin set of pip list, pip show, pip freeze and requirements.txt replayed into a second environment; wheels, sdists and the build backend, pip download, editable installs; the pip cache, offline installs, upgrading pip itself and pip uninstall; then pyproject.toml, pip check, publishing to PyPI, and the wider field.
Set in Space Grotesk, Inter and JetBrains Mono (SIL Open Font License).
Every fact in this book is as the official source states it, fetched and read during this build: the pip user guide, CLI reference, pip install page and pip freeze page from pip.pypa.io; the Python venv and installing documentation (docs.python.org); the Python Packaging User Guide (packaging.python.org); PEP 668, Marking Python base environments as externally managed (peps.python.org); the virtualenv documentation (virtualenv.pypa.io); and the Wikipedia article on pip. No facts are sourced from Reddit, Stack Overflow or vendor blogs. Every command and output pair on every page was run on this machine (Python 3.14.9, pip 26.2.2, Debian system Python 3.12.3 marked EXTERNALLY-MANAGED) and confirmed before printing; every install went into throwaway virtual environments under a scratch directory, never into a system or shared interpreter. The --break-system-packages option and publishing to PyPI were never run, and are stated from the sources instead. An independent guide, not affiliated with or endorsed by the Python Software Foundation, the Python Packaging Authority, the pip developers, or the Wikimedia Foundation.
Your purchase is for personal use only. You do not have redistribution rights: please do not share, resell, or republish this book or its pages.
© 2026 Steve Hodgkiss. All rights reserved. Personal use only; no redistribution rights.
Edition 1.0 · stevehodgkiss.net
Contents
Per the Wikipedia pip article, pip is a package manager written in Python, used to install and manage software packages, Python 3.4 and later include pip by default, and the name is a recursive acronym, Pip Installs Packages. This machine's capture: the venv's own interpreter reported pip 26.2.2 from the venv's site-packages, bound to python 3.14.
pip, the installer
pip installs and manages packages, and Python 3.4+ ships it by default, says the Wikipedia article. The name is a recursive acronym: Pip Installs Packages.
Whose pip answered? Our capture: pip 26.2.2 from the venv's own site-packages, on python 3.14. Running python -m pip names the interpreter, so you always know.
A bare pip can belong to anyone. The habit this book builds: one venv per project, and its python -m pip does everything.
Run python -m pip --version in two different venvs and watch the paths differ.
Per the Python venv documentation, the venv module supports creating lightweight virtual environments, each with their own independent set of Python packages installed in their site directories; creating one places a pyvenv.cfg file with a home key pointing to the Python installation from which the command was run, a bin (Scripts on Windows) subdirectory containing a copy or symlink of the interpreter, and a lib/pythonX.Y/site-packages subdirectory. This machine's capture: python3 -m venv created demo1 in seconds, unprivileged.
python -m venv
python -m venv demo1 builds a whole private environment: bin/ with python and pip, lib/.../site-packages/, and pyvenv.cfg.
The docs call it lightweight: each venv holds an independent set of packages in its own site directory. Our capture created one in seconds, no root, no apt.
pyvenv.cfg's home key names the base interpreter, so the venv is a thin profile over a real Python, not a copy of the language.
Create a throwaway venv, list its three parts, and read its pyvenv.cfg top to bottom.
Per PEP 668, Marking Python base environments as externally managed, a distribution marks its Python with an EXTERNALLY-MANAGED marker file so installers refuse to install packages into it outside the system package manager. Per the Python installing documentation, ensurepip bootstraps pip into an environment. This machine's capture: venv creation under the system Python refused with ensurepip is not available, advising apt install python3.12-venv, and the marker file /usr/lib/python3.12/EXTERNALLY-MANAGED is present.
The PEP 668 refusal
PEP 668 marks distro Pythons as externally managed: a marker file tells installers to keep their hands off.
Our machine shows both edges: the marker at /usr/lib/python3.12/EXTERNALLY-MANAGED, and a venv attempt refused with ensurepip is not available, pointing at apt.
That error you may have met, externally-managed-environment, is this policy in pip's voice. It is protection, not obstruction.
Look for an EXTERNALLY-MANAGED file under your system Python's lib directory; read its note.
Per the pip freeze page, pip freeze outputs installed packages in requirements format. This machine's capture: pip freeze in the venv printed exactly one line, six==1.17.0, the name, a double equals and the exact installed version.
pip freeze
pip freeze prints installed packages in requirements format: name, double equals, exact version. Our whole capture: six==1.17.0.
One line per package, no ranges, no comments on your behalf. What was installed, precisely, is what gets written.
Freeze records what IS, not what SHOULD be. Edit requirements by hand when you mean to change the plan; freeze only ever photographs it.
Freeze a real project's venv, read every line, and check each pin against what you intended.
Per the Python Packaging User Guide specification on the wheel binary distribution format, a wheel is a ZIP-format archive with a specially formatted file name and the .whl extension, containing a single distribution nearly as it would be installed, with the file name carrying the distribution, version, python tag, abi tag and platform tag. This machine's capture: pip download saved six-1.17.0-py2.py3-none-any.whl, the py2.py3-none-any tags marking a pure-Python wheel.
Wheels
A wheel is a ZIP-format archive with a specially formatted name and the .whl extension, says the specification: a single distribution, nearly as it would be installed.
The name is a passport: six-1.17.0-py2.py3-none-any is distribution, version, then python, abi and platform tags. none-any means pure Python, runs everywhere.
No build step, no compiler, no setup dance: a wheel is finished goods, and pip's favorite currency.
Download one wheel and one tar.gz of the same package; unzip the wheel and read its top level.
Per the pip CLI reference, pip cache is the subcommand for managing pip's local caches. This machine's captures: pip cache dir printed /root/.cache/pip, and pip cache info reported the http-v2 index page cache at 226.8 MB across 500 HTTP files, and locally built wheels at 926 kB, 2 of them.
The pip cache
pip keeps what it earns: /root/.cache/pip in our capture, holding an http-v2 index cache (226.8 MB, 500 files) and locally built wheels (926 kB).
That is why round two printed Using cached: no network, no rebuild. The pip cache subcommand inspects and purges it.
The cache serves bytes, never versions: your pins still decide what is right, the cache only makes it fast.
Run pip cache info, note the two sizes, and purge it once to feel what a cold install costs.
Per the Python Packaging User Guide packaging tutorial, publishing means learning how to build the package and how to upload it to the Python Package Index, generating distribution archives and running twine upload dist/*, with TestPyPI recommended for trying the flow first and a package name chosen that does not conflict with packages uploaded by other people. Publishing was not run on this machine during this build.
Publishing to PyPI
The tutorial's arc: build distribution archives from your project, then twine upload dist/* to the index.
Its two cautions: try TestPyPI first, and pick a name that does not conflict with other people's uploads.
Honesty note: not run here, no account exists on this machine. Every step shown is quoted from the tutorial, not improvised.
Walk the tutorial once against TestPyPI with a throwaway name; delete nothing, learn everything.