Python’s importlib.metadata module (Python 3.8+) reads metadata from installed packages without importing them. import importlib.metadata as ilm. Version: ilm.version("requests") → "2.31.0". All metadata: meta = ilm.metadata("requests") → email.message.Message; meta["Name"], meta["Author"], meta["Requires-Python"], meta.get_all("Classifier"). Dependencies: ilm.requires("requests") → list[str] | None — PEP 508 requirement strings. All packages: for dist in ilm.distributions(): — iterates every installed distribution. Entry points: eps = ilm.entry_points(group="console_scripts") → list of EntryPoint; ep.load() — import and return the callable. Files: ilm.files("pip") → list[PackagePath] | None. Package→dist mapping: ilm.packages_distributions() → dict[str, list[str]] (e.g. {"PIL": ["Pillow"]}). Error: PackageNotFoundError raised when a package is not installed. Claude Code generates dependency checkers, version validators, plugin registries, audit tools, and runtime environment reporters.
CLAUDE.md for importlib.metadata
## importlib.metadata Stack
- Stdlib: import importlib.metadata as ilm (Python 3.8+)
- Version: ilm.version("requests")
- Meta: meta = ilm.metadata("requests"); meta["Author"]
- Deps: ilm.requires("requests") # PEP 508 strings
- EPs: ilm.entry_points(group="console_scripts")
- All: for dist in ilm.distributions(): ...
- Error: ilm.PackageNotFoundError
importlib.metadata Distribution Pipeline
# app/metadatautil.py — version check, deps, entry points, audit, env report
from __future__ import annotations
import importlib.metadata as ilm
import re
import sys
from dataclasses import dataclass, field
from typing import Any
# ─────────────────────────────────────────────────────────────────────────────
# 1. Version helpers
# ─────────────────────────────────────────────────────────────────────────────
def pkg_version(name: str) -> "str | None":
"""
Return the installed version of a package, or None if not installed.
Example:
print(pkg_version("requests")) # "2.31.0"
print(pkg_version("nope_xyz")) # None
"""
try:
return ilm.version(name)
except ilm.PackageNotFoundError:
return None
def is_installed(name: str) -> bool:
"""Return True if the package is installed."""
return pkg_version(name) is not None
def version_tuple(name: str) -> "tuple[int, ...] | None":
"""
Return version as a tuple of ints for comparison, or None if not installed.
Example:
if version_tuple("requests") >= (2, 28, 0):
print("new enough")
"""
v = pkg_version(name)
if v is None:
return None
parts = re.split(r"[.+]", v.split("-")[0])
ints: list[int] = []
for p in parts:
if p.isdigit():
ints.append(int(p))
else:
break
return tuple(ints) if ints else None
def require_version(name: str, min_version: str) -> None:
"""
Raise RuntimeError if the installed version of name is below min_version.
Example:
require_version("requests", "2.28.0")
"""
installed = pkg_version(name)
if installed is None:
raise RuntimeError(f"Package {name!r} is not installed")
inst = version_tuple(name) or ()
req = tuple(int(p) for p in min_version.split(".") if p.isdigit())
if inst < req:
raise RuntimeError(
f"{name} {installed} < required {min_version}"
)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Metadata access
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class PackageMeta:
"""Key metadata fields for an installed package."""
name: str
version: str
summary: str
author: str
license: str
requires_python: str
classifiers: list[str] = field(default_factory=list)
requires: list[str] = field(default_factory=list)
home_page: str = ""
def package_meta(name: str) -> "PackageMeta | None":
"""
Return PackageMeta for an installed package, or None if not found.
Example:
meta = package_meta("pip")
if meta:
print(meta.version, meta.summary)
"""
try:
dist = ilm.distribution(name)
except ilm.PackageNotFoundError:
return None
m = dist.metadata
return PackageMeta(
name=m.get("Name", name),
version=m.get("Version", ""),
summary=m.get("Summary", ""),
author=m.get("Author", "") or m.get("Author-email", ""),
license=m.get("License", ""),
requires_python=m.get("Requires-Python", ""),
classifiers=m.get_all("Classifier") or [],
requires=ilm.requires(name) or [],
home_page=m.get("Home-page", "") or m.get("Project-URL", ""),
)
def all_installed() -> list[PackageMeta]:
"""
Return PackageMeta for every installed distribution, sorted by name.
Example:
pkgs = all_installed()
print(f"Installed: {len(pkgs)} packages")
"""
results: list[PackageMeta] = []
for dist in ilm.distributions():
m = dist.metadata
name = m.get("Name", "")
if not name:
continue
results.append(PackageMeta(
name=name,
version=m.get("Version", ""),
summary=m.get("Summary", ""),
author=m.get("Author", "") or m.get("Author-email", ""),
license=m.get("License", ""),
requires_python=m.get("Requires-Python", ""),
))
return sorted(results, key=lambda p: p.name.lower())
def package_for_import(import_name: str) -> "str | None":
"""
Return the distribution name that provides the given import package name.
Example:
package_for_import("PIL") # "Pillow"
package_for_import("requests") # "requests"
"""
mapping = ilm.packages_distributions()
dists = mapping.get(import_name)
return dists[0] if dists else None
# ─────────────────────────────────────────────────────────────────────────────
# 3. Entry points
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class EntryPointInfo:
"""Metadata about one entry point."""
group: str
name: str
value: str # "module:attr" string
dist: str
def load(self) -> Any:
"""Import and return the object pointed to by this entry point."""
ep = next(
e for e in ilm.entry_points(group=self.group) if e.name == self.name
)
return ep.load()
def list_entry_points(group: str) -> list[EntryPointInfo]:
"""
Return all entry points in a group with their distribution names.
Example:
for ep in list_entry_points("console_scripts"):
print(ep.name, "→", ep.value, "(from", ep.dist + ")")
"""
results: list[EntryPointInfo] = []
for ep in ilm.entry_points(group=group):
dist_name = ep.dist.name if ep.dist else ""
results.append(EntryPointInfo(
group=group,
name=ep.name,
value=ep.value,
dist=dist_name,
))
return sorted(results, key=lambda e: e.name)
def list_groups() -> list[str]:
"""
Return all entry point group names across all installed packages.
Example:
for group in list_groups():
print(group)
"""
return sorted({ep.group for ep in ilm.entry_points()})
# ─────────────────────────────────────────────────────────────────────────────
# 4. Environment audit
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class EnvReport:
"""Summary of the current Python environment."""
python_version: str
prefix: str
total_packages: int
packages_by_license: dict[str, int] # license → count
no_license: int
console_scripts: list[str]
def env_report() -> EnvReport:
"""
Build a summary report of the current Python environment.
Example:
r = env_report()
print(f"Python {r.python_version} {r.total_packages} packages")
for lic, count in sorted(r.packages_by_license.items(), key=lambda x: -x[1])[:5]:
print(f" {lic}: {count}")
"""
pkgs = all_installed()
by_license: dict[str, int] = {}
no_lic = 0
for p in pkgs:
lic = p.license.strip() or ""
if not lic:
no_lic += 1
else:
by_license[lic] = by_license.get(lic, 0) + 1
scripts = [ep.name for ep in list_entry_points("console_scripts")]
return EnvReport(
python_version=sys.version.split()[0],
prefix=sys.prefix,
total_packages=len(pkgs),
packages_by_license=by_license,
no_license=no_lic,
console_scripts=sorted(scripts),
)
def missing_packages(names: "list[str]") -> list[str]:
"""
Return names from the list that are NOT installed.
Example:
missing_packages(["requests", "nope_xyz", "pip"]) # ["nope_xyz"]
"""
return [n for n in names if not is_installed(n)]
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== importlib.metadata demo ===")
# ── version helpers ───────────────────────────────────────────────────────
print("\n--- pkg_version / is_installed / version_tuple ---")
for name in ["pip", "setuptools", "nonexistent_xyz_pkg"]:
v = pkg_version(name)
vt = version_tuple(name)
print(f" {name:20s} installed={is_installed(name)} version={v!r} tuple={vt}")
# ── package_meta ──────────────────────────────────────────────────────────
print("\n--- package_meta ---")
for name in ["pip", "setuptools"]:
meta = package_meta(name)
if meta:
print(f" {meta.name} {meta.version}")
print(f" summary: {meta.summary[:60]!r}")
print(f" python: {meta.requires_python!r}")
print(f" requires: {len(meta.requires)} deps")
# ── all_installed ─────────────────────────────────────────────────────────
print("\n--- all_installed (first 5) ---")
all_pkgs = all_installed()
print(f" total: {len(all_pkgs)} packages")
for p in all_pkgs[:5]:
print(f" {p.name:25s} {p.version}")
# ── entry_points ──────────────────────────────────────────────────────────
print("\n--- list_entry_points (console_scripts, first 5) ---")
scripts = list_entry_points("console_scripts")
print(f" total console_scripts: {len(scripts)}")
for ep in scripts[:5]:
print(f" {ep.name:20s} {ep.value!r} ({ep.dist})")
print("\n--- list_groups (first 8) ---")
groups = list_groups()
print(f" total groups: {len(groups)}")
for g in groups[:8]:
print(f" {g}")
# ── env_report ────────────────────────────────────────────────────────────
print("\n--- env_report ---")
report = env_report()
print(f" Python: {report.python_version}")
print(f" Packages: {report.total_packages}")
print(f" Scripts: {len(report.console_scripts)}")
print(f" No-license pkgs: {report.no_license}")
top_licenses = sorted(report.packages_by_license.items(), key=lambda x: -x[1])[:3]
print(f" Top licenses: {top_licenses}")
# ── missing_packages ──────────────────────────────────────────────────────
print("\n--- missing_packages ---")
missing = missing_packages(["pip", "setuptools", "nope_xyz_a", "nope_xyz_b"])
print(f" missing: {missing}")
print("\n=== done ===")
For the pkg_resources (setuptools) alternative — pkg_resources.require("requests>=2.0"), pkg_resources.get_distribution("pip").version, and pkg_resources.iter_entry_points("console_scripts") provide similar functionality but import the full setuptools machinery on startup — always prefer importlib.metadata for new code; it is stdlib, faster to import, and covers 100% of pkg_resources use cases for reading metadata (not for building/installing packages). For the packaging (PyPI) alternative — packaging.version.Version("1.2.3") >= packaging.version.Version("1.0.0") provides PEP 440-compliant version comparison that handles pre-releases, post-releases, and local versions correctly — combine importlib.metadata.version() with packaging.version.Version when you need rigorous semver or PEP 440 comparison; the version_tuple() helper in this pipeline handles simple numeric versions only. The Claude Skills 360 bundle includes importlib.metadata skill sets covering pkg_version()/is_installed()/version_tuple()/require_version() version helpers, PackageMeta/package_meta()/all_installed()/package_for_import() metadata access, EntryPointInfo/list_entry_points()/list_groups() entry point tools, and EnvReport/env_report()/missing_packages() audit utilities. Start with the free tier to try distribution metadata patterns and importlib.metadata pipeline code generation.