π§° Python Package Management with pip
PyPI hosts over 600,000 packages β a web framework, a data library, an HTTP client for almost anything you can name. pip is the tool that fetches, installs, and pins them. Master it and you rarely reinvent the wheel; misuse it and you invite conflicts and security holes.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain what PyPI and pip are and how they work together
- Install, upgrade, inspect, and uninstall packages with the core pip commands
- Read and write version specifiers (
==,>=,~=) correctly - Manage a project with
requirements.txtand layered requirement files - Reduce supply-chain risk with pinning, hashes, and vulnerability scanning
Estimated Time: 35β45 minutes β’ Difficulty: BeginnerβIntermediate
Hands-on: Set up a small Flask project with layered, pinned, security-scanned dependencies.
In This Lesson
pip and PyPI
pip (the "Pip Installs Packages" installer) is the standard tool for adding third-party code to a Python project. It downloads packages from PyPI β the Python Package Index at pypi.org β the public "app store" for Python libraries, hosting well over 600,000 projects.
600k+ packages)] Pip -->|installs into| Env[.venv site-packages] Pip -->|reads / writes| Req[requirements.txt]
π Key Terms
Package: reusable Python code published so others can install it (e.g. requests, django).
Wheel (.whl): a pre-built distribution format that installs fast with no compilation step.
Dependency: a package your package needs. pip installs these automatically.
pip ships with Python (3.4+), so it's almost always already there. Always run it against an activated virtual environment (previous lesson) so installs stay isolated. Verify and update it with:
# Check pip's version (both forms work)
pip --version
python -m pip --version
# Keep pip itself up to date
python -m pip install --upgrade pip
π‘ python -m pip vs bare pip
Prefer python -m pip .... It guarantees you're using the pip that belongs to that specific Python, which avoids the classic confusion of installing into one interpreter while running another.
Essential pip Commands
A handful of commands cover almost all day-to-day work.
Installing
# Install the latest version
pip install requests
# Install an exact version
pip install requests==2.31.0
# Install within a range
pip install "requests>=2.28,<3.0"
# Install everything a project needs
pip install -r requirements.txt
# Install your own project in "editable" mode (changes take effect live)
pip install -e .
Inspecting
pip list # everything installed
pip show requests # details about one package
pip list --outdated # what has newer versions available
pip freeze # installed packages in requirements format
Uninstalling
pip uninstall requests
β οΈ pip search is gone
The old pip search command was disabled because it overloaded PyPI's servers. To find packages, browse pypi.org directly. To list available versions of a known package, use pip index versions requests.
Version Specifiers
When you declare a dependency, you also declare which versions are acceptable. Getting this right is the difference between "works on my machine" and reproducible builds.
| Operator | Example | Meaning |
|---|---|---|
== | requests==2.31.0 | Exactly this version |
>= | requests>=2.31.0 | This version or newer |
< | requests<3.0.0 | Anything below 3.0.0 |
!= | requests!=2.30.0 | Any version except this one |
~= | requests~=2.31.0 | "Compatible release": >=2.31.0, <2.32.0 |
| combined | requests>=2.28,<3.0 | At least 2.28 but below 3.0 |
π‘ Rule of thumb
For applications you deploy, pin exact versions (or use a lock file) so production matches development. For libraries other people install, use ranges (>=, ~=) so your library plays nicely alongside their other dependencies.
Dependency Resolution
When you install a package, pip also installs everything it depends on β and everything those depend on. Since pip 20.3, a stricter resolver checks that the whole tree is mutually compatible, and reports an error if no combination works.
needs C>=1.0,<2.0] App --> B[Package B
needs C>=1.5] A --> C[Package C] B --> C
Here both A and B depend on C. The resolver looks for a version of C satisfying both constraints β for example C 1.6. If A demanded C<1.5 while B demanded C>=1.5, no version could satisfy both and pip would report a conflict.
β Avoiding "dependency hell"
- Always work inside a virtual environment so each project resolves independently.
- Pin versions for deployments; use ranges for libraries.
- Update dependencies regularly so you take small steps, not scary version leaps.
- For complex projects, let a lock file (Poetry, uv, pip-tools) resolve and record the full tree.
Managing requirements.txt
A requirements.txt file lists a project's dependencies so anyone can install them in one command and get the same environment.
# Snapshot the current environment (all packages, exact versions)
pip freeze > requirements.txt
# Recreate the environment elsewhere
pip install -r requirements.txt
For most applications, a hand-maintained file of direct dependencies is clearer than a raw freeze. You can also comment it:
# requirements.txt
Django>=4.2,<5.0
requests==2.31.0
python-dotenv>=1.0.0
psycopg2-binary==2.9.9 # PostgreSQL driver
Layered requirement files
Real projects separate concerns: shared deps, plus extras for development and production. Use -r to include one file from another.
# requirements-base.txt β needed everywhere
Django>=4.2,<5.0
requests>=2.31.0
python-dotenv>=1.0.0
# requirements-dev.txt β local development only
-r requirements-base.txt
pytest>=7.3
black>=23.3
ruff>=0.1
# requirements-prod.txt β production only
-r requirements-base.txt
gunicorn>=21.2
psycopg2-binary==2.9.9
# Developers install:
pip install -r requirements-dev.txt
# The production server installs:
pip install -r requirements-prod.txt
π‘ pip-tools and uv make this rigorous
Write only your top-level wants in a requirements.in, then compile a fully-pinned, hashed requirements.txt:
# with pip-tools
pip install pip-tools
pip-compile requirements.in # -> pinned requirements.txt
pip-sync requirements.txt # make the env match exactly
# uv does the same, much faster
uv pip compile requirements.in -o requirements.txt
Supply-Chain Security
Installing a package runs someone else's code with your permissions. That convenience is also a risk vector, so treat dependencies with care.
β οΈ Common threats
- Typosquatting: malicious packages with names close to popular ones (
reqeusts,python-dotenv-). Double-check the exact name. - Compromised maintainers: a legitimate package's account is hijacked and a bad release published.
- Dependency confusion: a public package is uploaded with the same name as your internal one.
- Known vulnerabilities: a dependency has a published CVE you haven't patched.
Defenses
- Pin versions in production so a surprise release can't slip in unnoticed.
- Scan for vulnerabilities regularly with
pip-audit. - Verify integrity with hashes so a tampered file is rejected at install time.
- Avoid
sudo pipβ install into a virtual environment, not system Python.
# Scan installed packages for known vulnerabilities
pip install pip-audit
pip-audit
# Or scan a requirements file
pip-audit -r requirements.txt
Hash-checking mode makes pip refuse any file whose contents don't match the recorded hash:
# requirements.txt with a pinned hash
requests==2.31.0 \
--hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
Tools like pip-compile --generate-hashes or uv produce these files for you.
Hands-on Exercise
ποΈ Set up a project with layered, secured dependencies
Objective: Build a small Flask project the professional way β isolated environment, layered requirement files, pinned versions, and a security scan.
Instructions
- Create and activate a virtual environment.
- Install Flask, requests, and python-dotenv.
- Write layered requirement files (base / dev / prod).
- Freeze a fully-pinned lock file and run a vulnerability scan.
mkdir weather_app && cd weather_app
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# Install direct dependencies
pip install flask requests python-dotenv
# requirements-base.txt
flask>=3.0,<4.0
requests>=2.31,<3.0
python-dotenv>=1.0,<2.0
# requirements-dev.txt
-r requirements-base.txt
pytest>=7.3
ruff>=0.1
# requirements-prod.txt
-r requirements-base.txt
gunicorn>=21.2
# Produce a fully-pinned snapshot and scan it
pip freeze > requirements-lock.txt
pip install pip-audit
pip-audit -r requirements-lock.txt
π‘ Hint
Add a .gitignore containing .venv/, __pycache__/, and .env so you never commit the environment or secrets. Commit the requirement files, not the .venv folder.
β What success looks like
pip list shows Flask, requests, and python-dotenv plus their dependencies. pip-audit reports "No known vulnerabilities found" (or names any it finds so you can bump the version). You now have a base/dev/prod split and a pinned lock file β exactly how production teams manage dependencies.
π― Quick Quiz
Question 1: What does requests~=2.31.0 allow?
Question 2: Which command captures the current environment's packages into a file you can reinstall from?
Question 3: Which practice best reduces supply-chain risk from your dependencies?
Best Practices
β Do
- Run pip inside an activated virtual environment, and prefer
python -m pip. - Pin versions (or use a lock file) for anything you deploy.
- Keep layered requirement files (base/dev/prod) and commit them to version control.
- Run
pip-auditin CI so vulnerabilities fail the build. - Update dependencies in small, regular steps.
β οΈ Don't
- Don't
sudo pip installinto system Python β it risks your OS and skips isolation. - Don't leave dependencies unpinned in production; a surprise release can break or compromise you.
- Don't blindly copy install commands β verify the exact package name to dodge typosquats.
- Don't commit your
.venvfolder or.envsecrets.
π‘ Beyond pip
For larger projects, higher-level tools build on the same foundation: Poetry (dependency resolution, lock files, packaging), pip-tools (compile pinned/hashed files), and uv (a Rust-based, drop-in-fast replacement for pip and venv). Learn pip first β the concepts transfer directly to all of them.
Summary & Quiz
π Key Takeaways
- pip installs packages from PyPI into your active environment; prefer
python -m pip. - Version specifiers (
==,>=,~=) control which versions are acceptable β pin apps, range libraries. - pip's resolver installs the whole dependency tree and flags conflicts it can't satisfy.
- requirements.txt (ideally layered base/dev/prod) makes environments reproducible.
- Guard the supply chain: pin versions, verify hashes, and scan with
pip-audit.
π Further Reading
- pip Documentation
- Python Packaging User Guide
- pip-audit
- uv Documentation
- Real Python β What Is pip?
π What's Next?
You've now covered Python's backend landscape, isolated environments, and dependency management. Next we cross into a different ecosystem with the same fundamentals: PHP for Backend Development.
π Nicely done!
Environments plus disciplined package management is the professional Python baseline β you've got both.