Pip is the standard Python package manager. Used well, it makes your project's dependencies reproducible across machines. Used poorly, it leads to drift, broken builds, and security surprises. This lesson covers the day-to-day pip commands, requirements files with pip-tools, and how to publish your own package to PyPI.
The pip commands you will use every day
pip install adds a package. pip list shows what is installed. pip show reveals details about one package. pip uninstall removes it. pip --version confirms the install. Run pip from inside an active virtual environment so the install goes into the right site-packages.
(.venv) $ pip install requests
Successfully installed certifi-2024.7.4 charset-normalizer-3.3.2 idna-3.7 requests-2.32.3 urllib3-2.2.2
(.venv) $ pip list
Package Version
------------------ --------
certifi 2024.7.4
requests 2.32.3
(.venv) $ pip show requests
Name: requests
Version: 2.32.3
Requires: charset-normalizer, idna, urllib3, certifi
Pin your dependencies
Always pin exact versions in production deployments. The cleanest way is two files: requirements.in (the direct dependencies you care about) and requirements.txt (the full pinned tree that pip actually installs). pip-tools compiles one from the other.
# requirements.in: the direct dependencies
requests
fastapi
pydantic>=2.0
# Compile to a fully-pinned requirements.txt
(.venv) $ pip install pip-tools
(.venv) $ pip-compile requirements.in
# Install the pinned set on another machine
(.venv) $ pip-sync requirements.txt
Private package indexes and mirrors
Large companies often host an internal PyPI mirror for approved packages. Configure pip to prefer it with --index-url. pip respects the order of index-url and extra-index-url and falls through if the first does not have a package.
# pip.conf in a virtualenv or in ~/.config/pip/pip.conf
[global]
index-url = https://pypi.org/simple
extra-index-url = https://download.pytorch.org/whl/cpu
Build and publish your own package to PyPI
Use the build package to create sdist and wheel distributions. Use twine to upload them to PyPI. Use a PyPI API token for authentication and never commit the token to git. TestPyPI is a separate index for trying uploads without polluting production.
# Read pyproject.toml and confirm the metadata is well-formed
import tomllib
with open("pyproject.toml", "rb") as f:
data = tomllib.load(f)
proj = data["project"]
assert proj["name"], "project.name is required"
assert proj["version"], "project.version is required"
print(f"Found {proj['name']} {proj['version']} with {len(proj.get('dependencies', []))} deps")
(.venv) $ pip install build twine
(.venv) $ python -m build
(.venv) $ twine upload dist/*
Uploading distributions to https://upload.pypi.org/legacy/
Constraints and hashes for supply-chain safety
For sensitive deployments, hash-pin every package so a tampered upload cannot sneak in. Generate hashes with pip-compile --generate-hashes and verify on install with pip install --require-hashes.
# Verify a wheel's hash matches the pinned entry before installing
import hashlib, urllib.request
url = "https://files.pythonhosted.org/packages/.../requests-2.32.3-py3-none-any.whl"
expected = "55365417734eb18255590a9ff9eb97e9e1da868d4ccd9602395342afbfec29ef"
sha = hashlib.sha256()
with urllib.request.urlopen(url) as r:
for chunk in iter(lambda: r.read(65536), b""):
sha.update(chunk)
if sha.hexdigest() != expected:
raise SystemExit("hash mismatch - refusing to install")
print("hash verified")
# requirements.txt with hashes
requests==2.32.3 \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd9602395342afbfec29ef
Common pitfalls with pip and packaging
Three traps catch newcomers. First, installing with sudo pip install pollutes the system Python and may break your OS package manager. Always work inside a venv so the install goes to project-local site-packages. Second, mixing pip and conda in the same environment produces hard-to-diagnose ImportError messages because the two package managers do not share a database. If you must use conda, install pip-only packages with pip only inside the conda env, never the other way round. Third, depending on an unpinned URL like git+https://... means your build can break any time the upstream repository moves a tag or rebases. Pin the commit hash and use a vendored wheel whenever possible.
Comparison with npm and Cargo
If you come from JavaScript or Rust, Python packaging is less polished but improving fast. npm installs dependencies into a flat node_modules at the project root, and JavaScript resolves modules through that tree. Rust uses Cargo with a single Cargo.toml that lists exact versions and a lockfile that pins the entire transitive graph. Python historically had no lockfile; pip-tools and uv have closed the gap. uv in particular is roughly ten times faster than pip and includes a built-in resolver. Cargo enforces semver at the package level, while pip only enforces operators you write in requirements.in. The trade-off is that Python libraries tend to declare looser version ranges, which makes dependency resolution a more common source of friction.
Real-world example: building and publishing a small library
The end-to-end flow for sharing a tiny library on PyPI is short enough to show in one screen. With hatchling as the build backend, the project layout and the publish commands fit on a few lines.
# Project layout
myhello/
├── pyproject.toml
├── src/
│ └── myhello/
│ └── __init__.py
└── tests/
└── test_basic.py
# Build and publish
$ python -m venv .venv && source .venv/bin/activate
$ pip install build twine hatchling
$ python -m build
$ twine upload dist/*
Test the upload on TestPyPI first by passing --repository testpypi to twine. Then install your package with pip install myhello to confirm the published wheel works in a fresh environment. Pin your library's dependencies with requires-python in pyproject.toml so pip refuses to install on unsupported Python versions, and document the supported range in the README so users know what to expect. Once you have a few releases under your belt, automate the publish with a GitHub Actions workflow on a version tag so manual uploads never ship a typo.
# src/myhello/__init__.py
__version__ = "0.1.0"
def greet(name: str) -> str:
return f"Hello, {name}"
__all__ = ["greet"]
This tiny module exports a single greet function and pins a version string. With this layout and a matching pyproject.toml that points at src/myhello, hatchling builds a clean wheel that pip install myhello can drop into any compatible environment.
Summary
Use pip inside a venv. Keep direct dependencies in requirements.in, pinned in requirements.txt, compiled with pip-tools. Use private indexes and hash-pinning for safety. Publish to PyPI with build and twine. The next lesson covers async programming with asyncio, Python's tool for high-concurrency I/O.