cheat sheet
importlib-metadata
Package-level reference for importlib-metadata on PyPI — install, distribution lookups, entry points, and the stdlib relationship from Python 3.10+.
importlib-metadata
What it is
importlib-metadata is a PyPI package that provides a stable API for reading installed-distribution metadata — package versions, entry points, file lists, requirements — from *.dist-info directories that pip writes into site-packages. The same module is in the standard library as importlib.metadata since Python 3.8, with the API stabilizing through 3.10. The PyPI package exists primarily as a backport so libraries can pin a single behaviour across Python versions, and as a place where new features land before being merged into the stdlib.
Reach for importlib-metadata when: you target Python versions older than 3.10 and need newer-than-stdlib features, you want a single import path that behaves identically across CPython releases, or you're writing a plugin host using entry points and want a guarantee-of-newness on the API. For new code on Python 3.10+ that doesn't need backporting, prefer the stdlib from importlib.metadata import ... and skip this package entirely.
Install
pip install importlib-metadata
Output: (none — exits 0 on success)
uv add importlib-metadata
Output: resolved + added to pyproject.toml
poetry add importlib-metadata
Output: updated lockfile + virtualenv install
Pure-Python wheel — no compile step. Adds one runtime dep on Python 3.9 (zipp); zero deps on Python 3.10+ because parts moved to stdlib.
Versioning & Python support
- Current line is the
7.x/8.xseries in 2025-26. - Supports Python 3.8+ on recent releases.
- The PyPI version tracks ahead of the stdlib version — features land here first, then are merged into the next CPython feature release. A library can
pip install importlib-metadata>=6and assume newer API regardless of the running Python. - Pin the major (
importlib-metadata>=7,<9) in libraries; applications on Python 3.10+ rarely need this package at all.
Package metadata
- Maintainer: Jason R. Coombs (the Python Packaging Authority maintainer for many adjacent packages)
- Project home: github.com/python/importlib_metadata
- Docs: importlib-metadata.readthedocs.io
- PyPI: pypi.org/project/importlib-metadata
- License: Apache-2.0
- Governance: PyPA / CPython core
- First released: 2019
- Downloads: hundreds of millions per month — a near-universal transitive dep
Optional dependencies & extras
importlib-metadata has no PyPI extras for users. Runtime deps depend on Python version:
- Python 3.9: depends on
zipp>=0.5(for reading metadata out of zipped wheels). - Python 3.10+: zero runtime deps; ships as a thin layer over
importlib.metadata.
Stdlib relationship
Python 3.10 stabilized the importlib.metadata stdlib API to match this package. Most code can write:
from importlib.metadata import version, entry_points, distributions
…and work without any PyPI install on 3.10+. The compatibility pattern for code that needs to target 3.8 / 3.9 too:
try:
from importlib.metadata import version, entry_points
except ImportError:
from importlib_metadata import version, entry_points # backport
Recent CPython feature releases (3.11, 3.12, 3.13) regularly pull updates from the PyPI package — entry-point grouping, selectable filters, the Distribution.read_text() semantics. If your code calls one of the newer methods on Python 3.10 specifically, prefer the backport package to side-step the version skew.
Alternatives
| Package | Trade-off |
|---|---|
importlib.metadata (stdlib, 3.10+) | Same API; no extra install. Use when you don't need to target 3.8/3.9 or newer-than-stdlib features. |
pkg_resources (setuptools) | Legacy; slow startup, large dependency. Avoid in new code. |
pkginfo | Reads metadata from sdists and wheels on disk; different use case (file-on-disk introspection). |
metadata (PEP 566 parser) | Lower-level; you usually don't want this directly. |
Common gotchas
- The import name is
importlib_metadata(underscore), the PyPI name isimportlib-metadata(hyphen). PEP 503 normalization. Easy to mistype inrequirements.txtvs Python. - Don't use
pkg_resourcesandimportlib.metadatatogether.pkg_resourceshas a 100-300ms cold-start cost and reads metadata its own way; mixing the two confuses caching layers. version("PkgName")is case-insensitive and accepts bothPkgNameandpkg-namevia PEP 503 normalization. Use whichever spelling is canonical for your project (lowercased-with-hyphens).entry_points()API changed in Python 3.10. Old form returned a dict; new form returns aSelectableGroupsobject. The backport keeps the new API consistently. If you mixfrom importlib.metadataon 3.10 with old code expecting a dict, behavior differs.Distribution.filesreturnsPackagePathobjects, not absolute paths. Convert withf.locate()for the real disk path.PackageNotFoundErroris raised, notModuleNotFoundError. They're different —version()looks up dist info, not importable modules. A namespace package's pieces may have multiple dists.- Editable installs (
pip install -e .) still produce*.dist-infoso the lookups work; sdist-only installs without metadata fail. - PYTHONDONTWRITEBYTECODE doesn't affect this; metadata reads .dist-info files directly off disk.
Real-world recipes
The recipes below cover the common cases: looking up a version, enumerating entry points for a plugin system, reading metadata, iterating distributions, and the stdlib-fallback pattern.
Recipe 1 — Read the installed version of a package.
try:
from importlib.metadata import version, PackageNotFoundError
except ImportError:
from importlib_metadata import version, PackageNotFoundError
try:
v = version("requests")
except PackageNotFoundError:
v = "unknown"
print(v)
Output: something like 2.32.3 — works on Python 3.8 through 3.13 with the same code.
Recipe 2 — Enumerate entry points (the canonical plugin-discovery API).
from importlib.metadata import entry_points
for ep in entry_points(group="console_scripts"):
print(ep.name, "→", ep.value)
Output: one line per installed CLI script, e.g. pip → pip._internal.cli.main:main.
# Project-defined plugin group
for ep in entry_points(group="myapp.plugins"):
plugin = ep.load() # imports the target and returns the object
plugin.register()
Output: each plugin module loaded and registered — the standard pattern for setup.cfg / pyproject.toml-declared plugin systems.
Recipe 3 — Read full metadata for a package.
from importlib.metadata import metadata
meta = metadata("requests")
print(meta["Name"], meta["Version"], meta["License"])
print(meta.get_all("Requires-Dist") or [])
Output: name, version, license, and the list of Requires-Dist lines from the wheel's METADATA file.
Recipe 4 — Iterate every installed distribution.
from importlib.metadata import distributions
for d in distributions():
print(d.metadata["Name"], d.version)
Output: one line per installed distribution — useful for "what's in this environment" tooling.
Recipe 5 — Distinguish from the stdlib (Python 3.10+).
import sys
if sys.version_info >= (3, 10):
from importlib.metadata import version
else:
from importlib_metadata import version # backport
# Even simpler — let pip's deps decide:
# importlib-metadata is conditionally pinned in pyproject.toml:
# importlib-metadata; python_version < "3.10"
Output: code stays terse; the conditional dep means production install on 3.10+ doesn't pull the backport.
Production deployment notes
- Pin conditionally. In
pyproject.toml:Avoid installing the backport on Python 3.10+ where stdlib already has it.dependencies = [ "importlib-metadata>=6; python_version < '3.10'", ] - Cache lookups.
version()andentry_points()re-scan site-packages each call. For plugin hosts with many calls, cache the results. - Beware lazy-loaded entry points in long-lived processes. Newly installed packages mid-process do NOT auto-register; restart or call the
*.cache_clear()helpers. - Don't ship
pkg_resourcesif you don't need it. Importingpkg_resourcesis slow (200-500ms cold) and rarely needed onceimportlib.metadatacovers the API surface.
Performance tuning
- First call to
entry_points()scans every*.dist-infoinsys.path. Subsequent calls are cached. For CLIs measuring startup, this scan adds milliseconds (small but real). distributions()is lazy. It yields a generator; iterate sparingly.- Replace
pkg_resources.iter_entry_pointswithimportlib.metadata.entry_points(group=...)for ~10× faster lookup. The Python ecosystem has been migrating offpkg_resourceslargely for this reason. - Use
version()notpkg_resources.get_distribution(...).version— half the runtime cost.
Version migration guide
< 4.x— older API shapes. Avoid.4.x → 5.x—entry_points()return type stabilized toSelectableGroups.5.x → 6.x— dropped Python 3.7.6.x → 7.x— minor; small API tightenings.7.x → 8.x— packaging modernization, no API change.
# Pre-5.0 entry-points (dict-style; deprecated)
from importlib.metadata import entry_points
eps = entry_points() # dict in old versions
console = eps["console_scripts"]
# 5.0+ (selectable API)
from importlib.metadata import entry_points
console = entry_points(group="console_scripts") # filtered selection
Output: the new form is dict-free and faster.
Security considerations
- Loading entry points executes Python code.
ep.load()imports the target module. Don't load entry points from untrusted environments (e.g. user-uploaded wheels). metadata()reads.dist-info/METADATAdirectly from disk. Don't trust the contents; a malicious package can lie about its name or version in metadata.- Distribution name parsing follows PEP 503. Be aware that
Foo,foo, andFoo-Barmay all normalize to the same canonical key — typosquatting risk if you treat names as primary identifiers. - No remote calls. This package is purely local-file I/O; no network attack surface.
Testing & CI integration
- For tests that inspect the running environment's metadata, isolate with a
pip install -t tmp/site-packages andsys.path.insert(0, "tmp"). - Use
pytest-virtualenvfor full-isolation tests of entry-point loading. - Mock with
monkeypatch.setattr(importlib.metadata, "version", lambda *_: "1.2.3")in unit tests.
import importlib.metadata as m
def test_my_version_callable():
# Force a specific reported version for the unit under test.
real = m.version
m.version = lambda name: "0.0.test"
try:
from myapp import version_string
assert "0.0.test" in version_string()
finally:
m.version = real
Output: test passes regardless of the actually-installed version.
Ecosystem integrations
setuptools— writes the*.dist-infodirectoriesimportlib.metadatareads.pip— installs wheels in the format this package reads.pluggy— pytest's plugin manager; can use entry points for plugin discovery.click— uses entry points for CLI registration in some patterns.pip-audit,pipdeptree— both consumeimportlib.metadatato enumerate installed packages.tox,hatch,pdm— use metadata to introspect environments.
Compatibility matrix
| Python | Recommendation |
|---|---|
| 3.7 | importlib-metadata PyPI required (stdlib doesn't have it). |
| 3.8 | stdlib works for basics; backport for newer features. |
| 3.9 | stdlib mostly OK; backport recommended for entry_points(group=...) consistency. |
| 3.10 | stdlib has the stabilized API; backport not needed unless you want newer-than-stdlib features. |
| 3.11+ | stdlib is canonical; install the backport only as a transitive dep. |
Troubleshooting common errors
| Error / Symptom | Likely cause | Fix |
|---|---|---|
PackageNotFoundError: No package metadata was found for 'foo' | Package not installed, or pip install -e left no metadata | Check pip show foo; reinstall with pip install. |
entry_points()['console_scripts'] raises TypeError | Calling old dict-style API on 5.0+ | Use entry_points(group="console_scripts"). |
| Slow CLI startup | Importing pkg_resources somewhere in the import chain | Migrate to importlib.metadata; cuts 200-500 ms. |
version("MyPkg") returns wrong version | Multiple installs in different sys.path entries | Inspect distributions() to find duplicates; clean the env. |
ImportError: cannot import name 'X' from importlib.metadata | Using a newer API on Python < 3.10 | Use the backport: from importlib_metadata import X. |
FileNotFoundError reading METADATA | Distribution metadata directory deleted manually | Reinstall the package with pip install --force-reinstall. |
EntryPoint.load() raises ModuleNotFoundError | Plugin declared but its package is uninstalled | Verify the plugin package is installed; entry-point declarations can outlive their package. |
When NOT to use this
- You're on Python 3.10+ and don't need newer-than-stdlib features. Use the stdlib directly.
import importlib.metadataadds nothing. - You need wheel-file introspection without installing. Use
pkginfoor parse the wheel directly. - You're using
pkg_resourcesfor a specific reason (legacy plugins, .egg files). Keep using it — but plan migration. - You need fast startup at any cost. Even
importlib.metadatadoes anos.scandir(); for ultra-fast CLIs, cache the answer at build time.
Worked example: a tiny plugin host using entry points
A common real-world use of importlib.metadata is plugin discovery. Two packages — a host that loads plugins, and one or more plugin packages that declare themselves via entry points.
Step 1 — the host package's interface.
# myapp/plugins.py
from typing import Protocol
class Plugin(Protocol):
name: str
def run(self, context: dict) -> None: ...
Output: a structural-typing contract for plugins — no inheritance required.
Step 2 — discover and load installed plugins.
# myapp/host.py
try:
from importlib.metadata import entry_points
except ImportError:
from importlib_metadata import entry_points
def load_plugins(group: str = "myapp.plugins") -> dict[str, "Plugin"]:
found: dict[str, Plugin] = {}
for ep in entry_points(group=group):
try:
obj = ep.load()
except Exception as e:
print(f"plugin {ep.name} failed to load: {e}")
continue
found[ep.name] = obj
return found
Output: a name-keyed mapping of plugin objects; load failures don't crash the host.
Step 3 — a plugin package declares itself.
# myapp-greeter/pyproject.toml
[project]
name = "myapp-greeter"
version = "1.0.0"
[project.entry-points."myapp.plugins"]
greeter = "myapp_greeter:plugin"
Output: when pip install myapp-greeter runs, the entry point is recorded in *.dist-info/entry_points.txt.
Step 4 — the plugin's module.
# myapp_greeter/__init__.py
class Greeter:
name = "greeter"
def run(self, context):
print(f"Hello, {context.get('user', 'world')}!")
plugin = Greeter()
Output: the entry point points at plugin = Greeter() so the host gets an instance, not a class.
Step 5 — wire it into the host's CLI.
# myapp/__main__.py
from myapp.host import load_plugins
plugins = load_plugins()
for name, plugin in plugins.items():
plugin.run({"user": "Alice Dev"})
Output: running python -m myapp after pip install myapp-greeter prints the plugin's greeting — no host-side code changes required to add new plugins.
FAQ
Q: How do I version-check at startup ("require pip >= 24")? A:
from importlib.metadata import version
from packaging.version import Version
if Version(version("pip")) < Version("24"):
raise SystemExit("pip 24+ required")
Q: Why is my version("MyPackage") returning a 0.0.0 placeholder?
A: Almost certainly an editable install (pip install -e .) where setuptools_scm couldn't compute the version. Set SETUPTOOLS_SCM_PRETEND_VERSION=1.2.3 for the build, or fall back to reading __version__ from your package.
Q: Are entry points safe to load from anywhere?
A: They execute Python code on .load(). Treat entry points like any third-party plugin system — only the packages installed in the active environment are loaded, and that's still arbitrary code execution at startup.
Q: How do I avoid pulling pkg_resources into a CLI's import path?
A: Avoid import pkg_resources anywhere. Use importlib.metadata instead. Tools like pyinstaller and nuitka benefit (smaller binary, faster startup).
Q: Does entry_points() work for packages installed with --user?
A: Yes — it scans every sys.path entry. Custom --target installs work too, as long as the directory is on sys.path.
Q: How do I get the install location of a package? A:
from importlib.metadata import distribution
d = distribution("requests")
print(d.locate_file(""))
Output: the directory containing the package on disk.
See also
- Concept: API — designing introspection APIs