Why read a package version with `importlib.metadata.version()` rather than `__version__`?
answer
- Two numbers, only one is authoritative
- One is a convention, one is metadata
- The argument name may surprise you
- Fails loudly outside an install
- Single-source it so nothing drifts
basics
~20 simportlib.metadata.version() reads the version recorded in the installed distribution's metadata — the same number the installer and resolvers see. A __version__ attribute is only a convention: it may be missing, and it can drift from what is actually installed.
solid answer
~40 s`__version__` is a module attribute someone wrote by hand; nothing requires a package to define it and nothing keeps it in step with reality. `importlib.metadata.version("chat-archiver")` instead reads the `Version` field of the installed distribution's metadata, which is what the installer wrote and what dependency resolvers and `pip list` report. Two gotchas matter. Its argument is the **distribution** name, which often differs from the **import** name — `importlib.metadata.packages_distributions()` maps one to the other. And it raises `importlib.metadata.PackageNotFoundError` when the code is running from a source checkout that was never installed, so a library that exposes `__version__` should catch that. The durable fix is single-sourcing: declare the version once, mark it `dynamic` in `pyproject.toml`, and derive the other side from it so the two cannot disagree.
code
python · 6 linesfrom importlib.metadata import version, PackageNotFoundError
try:
print("installed version:", version("pip"))
except PackageNotFoundError:
print("distribution is not installed")go deeper
Know that importlib.metadata.version() reports what is actually installed and that __version__ is only a convention some packages follow. Being able to print a dependency's version without a third-party helper is the bar here.
Explain where each number comes from, that the argument is the distribution name rather than the import name, and that a source checkout with no install raises PackageNotFoundError. Describe one single-sourcing scheme.
Show how you stop the two numbers drifting across releases, how a service reports the versions it is running for support, and how you avoid a metadata lookup crashing an import in a development checkout.
Own the policy: what your published version means, where it is authoritative, how build tooling derives it, and how the number reaching logs, telemetry and support tickets is guaranteed to be the one that was actually installed.
## Two different numbers with the same name A Python project has, potentially, two version strings. One is the **distribution metadata version**: the `Version` field written into the installed `.dist-info` directory when the package was installed, sourced from the project's build configuration. It is what the installer resolves against, what `pip list` prints, and what a dependency specifier like `chat-archiver>=2.3` is compared with. The other is `__version__`, an ordinary module attribute that some author typed into the source. Nothing in Python couples them. `importlib.metadata.version(name)` reads the first. It is in the standard library (since Python 3.8, with the API settling in 3.10), so it needs no third-party dependency: ```python from importlib.metadata import version, PackageNotFoundError try: installed = version("chat-archiver") except PackageNotFoundError: installed = "unknown (running from a source tree)" ``` ## Why `__version__` is the weaker source It is a convention, not a rule. Plenty of packages never define it, so `pkg.__version__` is an `AttributeError` waiting to happen in generic code that reports what is installed. Even where it exists it can disagree with the installed metadata, and the disagreement is easy to create: someone bumps the number in `pyproject.toml` for a release and forgets the module attribute, or the reverse. In a working copy installed in editable mode the trap is sharper: the `.dist-info` metadata is written once at install time, so after you bump the version in the project configuration and keep working without reinstalling, `importlib.metadata.version()` returns the stale cached value from that earlier install while the source tree says something newer. Either number can be the lying one; what you must not do is assume they agree. Reading `__version__` also costs an import of the package itself, which for a large library means executing its whole `__init__`. `importlib.metadata.version()` only reads metadata files, so a CLI that reports versions of several plugins does not have to import them all. ## The distribution name is not the import name This is the mistake that actually shows up in code review. The argument to `importlib.metadata.version()` is the *distribution* name — the project name on the index, the one in a requirements line — not the name you `import`. They routinely differ: a distribution may install a differently-named package, or several packages, or one package may be provided by a distribution with a hyphenated or prefixed name. Passing the import name yields `PackageNotFoundError` even though the code is plainly installed. When you need the mapping at runtime, `importlib.metadata.packages_distributions()` returns a dict from top-level import name to the list of distributions providing it. Related metadata is reachable the same way: `importlib.metadata.metadata(name)` returns the full metadata mapping (summary, requires, classifiers), and `importlib.metadata.distribution(name)` gives an object exposing its files and requirements. Reporting a bug from a running service is much better served by those than by whatever the author remembered to hand-write. ## Single-sourcing so the two cannot drift The clean resolution is to have one place the version lives. Two shapes are common. Either declare it statically in `pyproject.toml` under `[project]` and, if the library wants a `__version__` at all, compute it at import time from `importlib.metadata.version("chat-archiver")` — the module attribute then simply reflects the installed metadata. Or keep the literal in the source and mark the project version `dynamic`, so the build backend reads the number out of the module (or out of a version-control tag) when the distribution is built. Both eliminate the hand-copy step; the first is usually simpler, the second suits projects that derive versions from tags. If you do expose `__version__` derived from metadata, guard it: running from a checkout that was never installed raises `PackageNotFoundError`, and a library that lets that escape from its `__init__` is unimportable in exactly the situation — a contributor's fresh clone — where you least want a crash. Falling back to a placeholder string, or to a development marker, keeps the package usable. Finally, remember what the number is *for*. It is a claim about compatibility that callers and resolvers act on. That is why it belongs in the metadata the installer reads, and why a hand-maintained attribute that quietly disagrees with it is worse than having none at all.
- Your library defines `__version__ = importlib.metadata.version("chat-archiver")` at import time. What breaks, and how do you handle it?A contributor running from a fresh clone that was never installed gets `PackageNotFoundError` raised out of the package's `__init__`, making the library unimportable. Wrap the lookup and fall back to a placeholder such as `"0.0.0.dev0"`. It also adds a small metadata read to every import, which matters only for very import-sensitive tools.
- How do you keep the version in `pyproject.toml` and the version in the source from diverging?Single-source it. Either keep the literal in `[project] version` and derive the module attribute from `importlib.metadata.version()`, or keep the literal in the module (or a version-control tag) and declare `dynamic = ["version"]` so the build backend reads it at build time. Either way one place is authoritative and no human copies a number between two files.
- Why can passing your package's import name to `importlib.metadata.version()` raise `PackageNotFoundError`?Because the function takes the distribution name — the project name recorded in the installed metadata — and distributions frequently install a package under a different name, or install several. The import name you type is not necessarily registered as a distribution at all. Use the distribution name from your project configuration, or resolve it at runtime with `packages_distributions()`.
saying these in an interview costs you the question
- Assumes every package defines __version__
- Passes the import name where the distribution name is required
- Believes __version__ is kept in sync automatically
- Lets PackageNotFoundError escape a package's __init__
- Imports a whole library just to report its version
- Maintains the number by hand in two separate files