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 or uv downloads from PyPI.
- An import package is what Python imports. It’s a directory of
.pyfiles that appears onsys.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 (a pre-built zip that installers can drop straight into a virtual environment) or a source distribution (the raw source tree plus a build recipe).
A distribution package has exactly one name, declared in the [project] table of pyproject.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 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).
A single .py file is a 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:
>>> 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>(oruv pip show -f <distribution>) prints installed metadata plus aFiles:section listing every file the distribution placed insite-packages. The top-level directories in that list are the import packages. Running it againstpillowshows files underPIL/, 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 maintains a 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 and Import Package
- What is PEP 503? covers PyPI name normalization rules
- What is a Python module? explains the difference between a module and an import package
- What is a Python package? covers the distribution side in more depth
- PEP 420: Implicit Namespace Packages defines how multiple distributions can share one import namespace
- wheel reference and sdist reference describe the two distribution formats
- pyproject.toml reference documents where the distribution name is declared
importlib.metadata.packages_distributions()in the standard library docs