# What's the difference between a distribution package and an import package?


You run `pip install Pillow`, open a REPL, and type `import Pillow`. Python raises `ModuleNotFoundError`. The actual incantation is `import PIL`. Nothing in the install output warned you.

This confusion has a precise answer: the word "package" means two different things in Python, and they're named independently.

- A distribution package is what you install. It's the archive [pip](https://pydevtools.com/handbook/reference/pip.md) or [uv](https://pydevtools.com/handbook/reference/uv.md) downloads from [PyPI](https://pydevtools.com/handbook/explanation/what-is-pypi.md).
- An import package is what Python imports. It's a directory of `.py` files that appears on `sys.path`.

One is a shipping container. The other is the thing inside. They carry different names for different reasons, and nothing in the ecosystem forces them to match.

## Pillow is not the only offender

Pillow is the case everyone hits first, but the name mismatch shows up across the most-installed packages on PyPI:

| Distribution package (what you install) | Import package (what you import) |
|---|---|
| `Pillow` | `PIL` |
| `beautifulsoup4` | `bs4` |
| `scikit-learn` | `sklearn` |
| `python-dateutil` | `dateutil` |
| `PyYAML` | `yaml` |
| `opencv-python` | `cv2` |

The rest of this page explains why these diverge, and how to look up the correct name when you only have one half of the pair.

## A distribution package is the shipping container

A distribution package is a versioned archive containing code plus metadata. In practice it's either a [wheel](https://pydevtools.com/handbook/reference/wheel.md) (a pre-built zip that installers can drop straight into a virtual environment) or a [source distribution](https://pydevtools.com/handbook/reference/sdist.md) (the raw source tree plus a build recipe).

A distribution package has exactly one name, declared in the `[project]` table of [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md):

```toml
[project]
name = "pillow"
version = "12.2.0"
```

That `name` is what gets registered on PyPI, what appears in `[project.dependencies]` when another project depends on it, and what you pass to `pip install` or `uv add`. It's normalized by [PEP 503](https://pydevtools.com/handbook/explanation/what-is-pep-503.md) so that `Pillow`, `pillow`, and `PILLOW` all resolve to the same project.

## An import package is a directory Python can find

An import package is what the language actually cares about. At runtime, when you write `import foo`, Python walks `sys.path` looking for a directory named `foo` containing an `__init__.py` file (a "regular package"), or a directory named `foo` that forms part of a namespace package under PEP 420 (see [peps.python.org/pep-0420](https://peps.python.org/pep-0420/)).

A single `.py` file is a [module](/handbook/explanation/what-is-a-python-module/), not a package. The distinction matters: packages can contain submodules (`PIL.Image`), modules can't.

The import system has never cared where a directory came from. It cares only whether the directory exists on `sys.path` and matches the name in your `import` statement. The installer's job is to put files in the right place; after that, Python's import machinery takes over and the distribution name is forgotten.

## Why the two names drift apart

The names diverge because they answer to different rules.

PyPI names live in a flat global namespace. Any project can claim any available name, subject to PEP 503 normalization. Dashes and underscores collapse, case is ignored. The name has to be unique across all of PyPI, but it doesn't have to be a valid Python identifier, which is how `scikit-learn` and `python-dateutil` exist at all.

Import names must be valid Python identifiers. `import scikit-learn` is a syntax error; the hyphen is subtraction. So `scikit-learn` ships an import package named `sklearn`, and `python-dateutil` ships one named `dateutil`. The project picks whatever valid identifier it wants.

Pillow is the famous case with a historical twist. It started as a friendly fork of the original Python Imaging Library ("PIL"), which had stopped receiving updates. Keeping the import name as `PIL` meant existing code could upgrade without changing a single `import` statement. The distribution got a new name on PyPI; the import name stayed put for backward compatibility.

## One distribution can ship many import packages

The relationship between the two isn't one-to-one in either direction.

A single distribution can install several top-level import packages. `pip install setuptools` puts both `setuptools` and `_distutils_hack` into `site-packages`, and older versions also shipped `pkg_resources` from the same archive. Each top-level directory is its own import package, all installed from one distribution.

Multiple distributions can also contribute submodules to the same namespace package. Microsoft's `azure-*` libraries are the canonical example. `pip install azure-storage-blob` and `pip install azure-identity` both install code under the shared `azure` namespace, and you import them as `from azure.storage.blob import BlobClient` and `from azure.identity import DefaultAzureCredential`. Neither distribution owns `azure`; they cooperate under PEP 420 namespace package rules.

Readers who assume "one install equals one import name" get tripped up by both cases. The safer mental model: a distribution can contribute any number of importable names, and any importable name may come from more than one distribution.

## Finding the import name from the distribution name

Python 3.10 added an authoritative programmatic lookup. `importlib.metadata.packages_distributions()` returns a dict mapping every top-level import name in the current environment to the list of distributions that provide it:

```python
>>> import importlib.metadata
>>> importlib.metadata.packages_distributions()
{'PIL': ['pillow'],
 'bs4': ['beautifulsoup4'],
 'sklearn': ['scikit-learn'],
 'dateutil': ['python-dateutil'],
 'yaml': ['PyYAML'],
 'cv2': ['opencv-python'],
 ...}
```

This works in both directions. Scanning the keys tells you which import name a distribution provides. Scanning the values tells you which distribution owns an import name. It's the only lookup that reflects what's actually installed, not what a README or PyPI page claims.

Two other techniques are worth knowing:

- `pip show -f <distribution>` (or `uv pip show -f <distribution>`) prints installed metadata plus a `Files:` section listing every file the distribution placed in `site-packages`. The top-level directories in that list are the import packages. Running it against `pillow` shows files under `PIL/`, which is the import name.
- PyPI project pages sometimes document the import name in the README, but this is unreliable. Many project pages assume you already know.

For a package that isn't installed yet, install it into a throwaway environment first, then run `packages_distributions()`. `uv run --with <distribution> python -c '...'` does this in one command without polluting your real environment.

## Name collisions are a real footgun

Because the import namespace is local to each environment, two distributions can both try to own the same import name. Nothing stops them. Whichever one you installed most recently wins, and the first one's files get overwritten without warning.

This has happened in the wild with `pil` (the original, abandoned PIL) vs `pillow` (both expose `PIL`), and more often with typo-squatted or forked packages that reuse a familiar import name. Running `packages_distributions()` and looking for import names that map to more than one distribution is a fast way to spot trouble. If the values list has length greater than one for a key that shouldn't be a namespace package, something is wrong.

## Why the PyPA glossary spells out both terms

The [Python Packaging Authority](https://pydevtools.com/handbook/explanation/what-is-pypa.md) maintains a [glossary](https://packaging.python.org/en/latest/glossary/) that formally distinguishes "Distribution Package" from "Import Package." It exists precisely because the bare word "package" is ambiguous, and tools like pip and uv drop the qualifier in their user-facing output. When a pip error message says "package not found," it means a distribution package on an index. When a Python traceback says `No module named 'foo'`, it means an import package on `sys.path`. Keeping the two concepts labeled separately is the cleanest way to avoid the confusion that starts with `pip install Pillow`.

## Learn More

- [PyPA glossary: Distribution Package](https://packaging.python.org/en/latest/glossary/#term-Distribution-Package) and [Import Package](https://packaging.python.org/en/latest/glossary/#term-Import-Package)
- [What is PEP 503?](https://pydevtools.com/handbook/explanation/what-is-pep-503.md) covers PyPI name normalization rules
- [What is a Python module?](/handbook/explanation/what-is-a-python-module/) explains the difference between a module and an import package
- [What is a Python package?](https://pydevtools.com/handbook/explanation/what-is-a-python-package.md) covers the distribution side in more depth
- [PEP 420: Implicit Namespace Packages](https://peps.python.org/pep-0420/) defines how multiple distributions can share one import namespace
- [wheel reference](https://pydevtools.com/handbook/reference/wheel.md) and [sdist reference](https://pydevtools.com/handbook/reference/sdist.md) describe the two distribution formats
- [pyproject.toml reference](https://pydevtools.com/handbook/reference/pyproject.toml.md) documents where the distribution name is declared
- [`importlib.metadata.packages_distributions()`](https://docs.python.org/3/library/importlib.metadata.html#importlib.metadata.packages_distributions) in the standard library docs
