Python's standard library covers a lot, but most real projects pull in code that someone else already wrote, things like HTTP clients, image processing, web frameworks, and data tools. pip is the tool that fetches those packages from PyPI (the Python Package Index) and installs them so you can import them in your code. This lesson covers what pip and PyPI are, the everyday commands you'll run, how to pin versions, how to share dependencies through requirements.txt, and the pitfalls that catch new users.
PyPI (the Python Package Index, pronounced "pie-pee-eye") is the public registry at pypi.org where Python developers publish open-source packages. Think of it as the central library for Python code that isn't part of the standard library. As of writing, PyPI hosts well over half a million projects, ranging from one-file utilities to massive frameworks.
pip is the command-line tool that talks to PyPI on your behalf. When you run pip install requests, pip contacts PyPI, downloads the requests package, resolves its dependencies, downloads those too, and installs everything into your Python environment so import requests works.
The relationship is simple: PyPI is the storehouse, pip is the delivery service.
The diagram shows the flow end-to-end. You type a command, pip queries PyPI for the latest matching version, downloads a wheel (a pre-built archive format Python uses for packages), unpacks it into the site-packages directory of your active Python environment, and from then on your code can import the package.
pip ships with Python itself on most modern installations. If you have Python 3.4 or newer, you almost certainly have pip too. You can check by running pip --version in your terminal.
The output shows the pip version, the path to the pip installation, and the Python version it's tied to. That last detail matters more than it looks; every pip installation belongs to a specific Python interpreter.
Use `python -m pip` to be safe. When you have multiple Python versions installed (very common), a bare pip command might run pip from a different interpreter than the python you intend to use. The safer form is python -m pip install <name>, which guarantees pip runs against the same interpreter as python. The rest of this lesson uses the shorter pip install form for readability, but python -m pip install is the form to reach for whenever you're unsure which Python is active.
The most common pip command is install. To install the requests library (a popular HTTP client used to fetch product images for E-Commerce examples):
A few things to notice in the output:
requests, but also four other packages (charset-normalizer, idna, urllib3, certifi). Those are transitive dependencies, packages that requests itself depends on. pip figures out the full dependency graph and installs everything needed for requests to work..whl file (wheel). Wheels are pre-built archives, so installing one is mostly a matter of unpacking.requests available on PyPI at the time the command ran.Once installed, you can use the package in any Python file or REPL session that runs on the same interpreter:
In an E-Commerce app, this is the pattern you'd use to fetch a product image from a CDN, check a remote stock API, or send an order confirmation through a webhook.
Most of what you'll do with pip falls into a handful of commands. Here's the everyday set:
| Command | What It Does |
|---|---|
pip install <name> | Installs a package (and its dependencies) from PyPI. |
pip install --upgrade <name> | Upgrades an already-installed package to the latest version. |
pip uninstall <name> | Removes a package. Does not remove its dependencies. |
pip list | Shows all installed packages and their versions. |
pip show <name> | Shows detailed metadata for one package (version, location, dependencies). |
pip freeze | Lists installed packages in requirements.txt format (pinned versions). |
pip check | Verifies that installed packages have compatible dependencies. |
Let's walk through each one with E-Commerce-flavored examples.
pip listpip list shows everything installed in the current environment. It's the quickest way to answer "do I have this package, and at what version?"
pip showpip show is the inspector. The Requires line tells you what the package depends on; Required-by tells you what else in your environment depends on this package (empty here because nothing else in our environment uses requests). The Location field tells you where the package files actually live on disk.
pip uninstallpip asks for confirmation by default. Add -y to skip the prompt: pip uninstall -y requests.
One thing worth flagging: pip uninstall requests removes only requests. It leaves urllib3, idna, charset-normalizer, and certifi behind, even though they were installed as dependencies of requests. pip doesn't track which packages were "asked for directly" versus "pulled in as a dependency", so it can't safely clean up orphans on its own. Tools like pip-autoremove or uv (mentioned later) handle that case.
pip freezepip freeze looks like pip list but in a different format. Each line is <name>==<version>, which is exactly the format pip install accepts. This is what makes pip freeze the foundation of requirements.txt (covered shortly).
Cost: pip freeze includes every installed package, both direct dependencies (the ones you asked for) and transitive ones (pulled in automatically). For a small project this is fine and even desirable; for a large project the output can stretch to hundreds of lines, and you lose track of which packages your code actually imports. Tools like pipdeptree or uv pip compile separate direct from transitive when that distinction matters.
pip checkpip check walks every installed package's metadata and verifies that the versions of its dependencies satisfy what the package declared it needs. If you have an environment where, say, package A needs urllib3>=2.0 but urllib3==1.26.0 is installed, pip check flags it:
Run pip check after a series of installs and upgrades to catch incompatibilities before your code blows up at runtime.
When you run pip install requests, you get the latest version on PyPI. But sometimes you need a specific version, or any version above a certain floor, or anything except a known-bad version. pip supports a small set of version specifiers that go right after the package name.
| Specifier | Meaning | Example | What It Allows |
|---|---|---|---|
== | Exactly equal | requests==2.31.0 | Only 2.31.0. Nothing else. |
>= | Greater than or equal | requests>=2.28 | 2.28, 2.31, 3.0, anything newer. |
< | Strictly less than | requests<3 | Any 2.x version. Blocks 3.0+. |
~= | Compatible release | requests~=2.31 | 2.31 and any later 2.x, but not 3.0. |
!= | Not equal | requests!=2.30.0 | Anything except 2.30.0. Useful for skipping a broken release. |
You can combine them with commas to express a range:
That asks for at least 2.28 but strictly less than 3.0. The quotes are needed in most shells because < and > are special characters; without them, the shell tries to redirect input or output and you get a confusing error.
Here are a few realistic E-Commerce scenarios:
The ~= specifier is worth slowing down on because it's the most useful and the most often misunderstood. requests~=2.31 is equivalent to requests>=2.31,<3.0. The general rule for ~=X.Y is "anything from X.Y up to but not including X+1.0". For ~=X.Y.Z, the upper bound is the next X.Y+1.0. So requests~=2.31.0 means >=2.31.0,<2.32.0, much tighter than ~=2.31. The dot count matters.
Cost: A version specifier that's too tight (like requests==2.31.0 everywhere) blocks security patches. Too loose (requests with no specifier) lets a breaking 3.0 release ship into production unannounced. The usual middle ground for libraries is ~= or >=X.Y,<X+1. For applications, pin exactly in requirements.txt and refresh on a schedule.
To upgrade a package to the latest version on PyPI:
pip uninstalls the old version and installs the new one. The short form is pip install -U requests.
You can pin a specific upgrade target:
Or upgrade multiple packages at once:
To upgrade pip itself:
The python -m pip form is the recommended way to upgrade pip, because pip can't reliably overwrite itself if it's the one running. Going through python -m pip sidesteps that issue.
A real project has more than one dependency, and you need a way to tell teammates (and your future self, and your deployment server) exactly what to install. The convention is a file named requirements.txt in the project root.
A simple requirements.txt for a small E-Commerce script might look like this:
Each line is one package, with an optional version specifier. Comments start with # and blank lines are ignored. To install everything in the file:
The -r flag stands for "requirements". You can have multiple requirements files (requirements-dev.txt, requirements-test.txt) and install them all separately.
To generate a requirements.txt from a working environment:
That redirects pip freeze's output into the file. Now anyone with the file can reproduce the same environment by running pip install -r requirements.txt.
There are two common philosophies for what to put in requirements.txt:
Pinned (exact versions):
Every version is locked. Anyone installing this gets exactly the same versions, which means exactly the same behavior. This is the safe choice for applications (your E-Commerce backend, a deployed web service, a script that runs in production).
Ranges (compatible versions):
This allows newer versions to be picked up automatically, which is useful for libraries (code other people will install and combine with their own dependencies). A library that pins exactly will conflict with anything else that pins a different version.
A common pattern is to have both: a loose set of direct dependencies in a pyproject.toml or requirements.in, and a tightly pinned requirements.txt (called a "lock file") that was generated from it. Tools like pip-tools, uv, and poetry formalize this two-file approach. The pip freeze output is the manual version of the same idea.
Cost: pip freeze > requirements.txt captures everything, including transitive deps and packages you installed once and forgot about. The list ends up larger than your direct dependency set, and pruning it manually is tedious. For a clean lock file, use a tool that knows which packages you asked for directly (uv pip compile, pip-tools, poetry lock).
Here's a full E-Commerce-themed example. Imagine a small product image downloader script that uses requests to fetch images and pillow to resize them:
The requirements.txt for this script:
A teammate clones the repo, runs pip install -r requirements.txt, and gets the exact same environment. The script works for them on the first try.
When pip installs a package, it copies the files into a directory called site-packages. The exact location depends on which Python interpreter pip is tied to and whether you're using a virtual environment.
You can ask Python where it looks for installed packages:
Inside a virtual environment, the same call returns a path inside the virtual environment's folder instead:
Three flavors of install location are worth knowing:
| Location | When It's Used | Pros | Cons |
|---|---|---|---|
| System site-packages | Plain pip install on a system-wide Python. | One install for everything. | Mixes dependencies across all projects. Hard to clean up. Often needs sudo. |
| User site-packages | pip install --user <name> (no virtualenv). | Doesn't need admin rights. | Still shared across all of your projects. |
| Virtual environment site-packages | pip install after activating a venv. | Isolated per project. Easy to throw away and rebuild. | One more thing to set up. |
The right default for almost every real project is the third option: a virtual environment per project. The takeaway is that "where does pip install things?" depends on which Python pip is running against, which is exactly why python -m pip install is the safer form.
Modern Python installations on macOS and Linux have started flagging system-wide installs with an error:
That's Python's way of saying "don't install into the system Python; make a virtualenv." It's a strong nudge toward the right default.
PyPI hosts hundreds of thousands of packages. You don't need to learn most of them, but a handful show up in nearly every Python codebase. Here's an orientation, not a deep dive:
| Package | What It Does | Where You'd Use It |
|---|---|---|
requests | HTTP client for talking to APIs and websites. | Fetching product images, calling payment APIs, scraping data. |
numpy | Fast numerical arrays and math. | Anything math-heavy: recommendations, pricing models, analytics. |
pandas | Data tables (DataFrames) and analytics. | Sales reports, order histories, CSV processing. |
pytest | Testing framework with simple syntax. | Writing tests for your cart, checkout, and pricing logic. |
rich | Pretty terminal output (colors, tables, progress bars). | Colorized cart printers, CLI dashboards, log formatting. |
click | Builds command-line interfaces. | Wrapping admin scripts (shop add-product, shop refund). |
flask | Lightweight web framework. | Small APIs, quick prototypes, internal tools. |
fastapi | Modern async web framework with type hints. | Production REST APIs, microservices. |
sqlalchemy | SQL toolkit and ORM. | Reading and writing to relational databases (orders, customers). |
beautifulsoup4 | HTML parser. | Scraping competitor pricing, extracting structured data. |
pillow | Image processing (fork of PIL). | Thumbnailing product photos, watermarking, format conversion. |
Here's a quick taste of rich for an E-Commerce cart printer:
That output is colored in a real terminal. The Markdown rendering can't show it, but in your shell you'll see cyan product names, green quantities, and orange prices. Forty lines of print statements collapse into something readable and easy to scan.
The point here is just to know these packages exist and that the install command is the same for all of them: pip install <name>.
A few patterns trip up most people who are new to pip. None of them are hard to avoid once you've seen them.
Running pip install without a virtualenv active drops the package into the system Python (or your user Python). Every project on your machine then sees that package, at that version. When Project A wants pandas==1.5 and Project B wants pandas==2.0, you're stuck.
The fix is to make a virtualenv per project. The rule is: if you're installing anything beyond a one-off learning script, do it inside a virtualenv.
If you're using conda (a popular Python distribution from Anaconda), you have two package managers on the same machine: conda and pip. They both write to the same site-packages directory inside a conda environment, but they don't know about each other. Installing the same package twice (once via conda, once via pip) can leave your environment in a half-broken state where pip check complains and imports behave inconsistently.
The pragmatic rule: in a conda environment, install with conda install first when the package is available through conda. Fall back to pip install only for packages conda doesn't have. Don't switch back and forth on the same package.
The name you pip install is not always the name you import. This catches almost everyone.
| pip install name | Python import name |
|---|---|
pillow | PIL |
beautifulsoup4 | bs4 |
pyyaml | yaml |
opencv-python | cv2 |
scikit-learn | sklearn |
python-dateutil | dateutil |
So you do:
And then:
Not import pillow. The package's authors chose a different import name for historical or namespacing reasons, and pip can't help you guess. The package's PyPI page or README almost always tells you the import name in the first code example.
When two of your packages need different versions of the same dependency, pip will try to find a version that satisfies both. If no such version exists, pip falls back to one of them and prints a warning, or in newer pip versions, refuses to install. The result is an environment that imports but breaks at runtime.
When this happens:
pip check to confirm the conflict.A virtualenv per project keeps these conflicts scoped. A conflict in one project doesn't break another.
You install a new package mid-project, your code starts importing it, you commit and push. A teammate pulls and runs the code, and it crashes with ModuleNotFoundError. The package was installed on your machine but never made it into requirements.txt.
The habit is: every time you pip install something new for the project, immediately regenerate the requirements file (pip freeze > requirements.txt) or add the line manually. Treat the requirements file as part of the code, not as a one-time setup artifact.
pip is the default, but it's not the only tool in this space. Three are worth knowing by name:
pyproject.toml. Heavier than pip but more opinionated.black, httpie, cookiecutter) in their own isolated environments so they don't pollute your project environments. Use it for command-line tools you want available everywhere.For this lesson, pip alone is enough. uv and poetry offer migration paths from a requirements.txt workflow.
10 quizzes