cheat sheet

packaging

Package-level reference for packaging on PyPI — Version, SpecifierSet, Requirement, markers, install, alternatives.

packaging

What it is

packaging is the reference implementation of the PyPA packaging standards. It exposes parsers and comparators for PEP 440 versions, PEP 508 environment markers and requirements, PEP 425 wheel-compatibility tags, and PEP 621 metadata. The library is used internally by pip, build, setuptools, poetry, and uv — wherever a Python tool needs to interpret a version string or requirement specifier, this is the canonical implementation.

Reach for packaging directly when you need to: compare two versions semantically (not lexically); check whether an installed package satisfies a requirement; build a tool that consumes requirements.txt or pyproject.toml; or canonicalize package names for case-insensitive matching.

Install

bash
pip install packaging

Output: (none — exits 0 on success; pure-Python, zero runtime dependencies)

bash
uv add packaging

Output: dependency resolved + added to pyproject.toml

bash
poetry add packaging

Output: updated lockfile + virtualenv install

No optional extras — packaging is a single pure-Python module.

Versioning & Python support

  • Releases are date-rooted but numerically increasing (23.x, 24.x, …). Major versions correspond to standards updates and may include API tweaks.
  • Supports Python 3.8+ on recent releases; older Pythons can use older packaging versions.
  • The library is bundled inside pip (vendored copy at pip._vendor.packaging), so even apps that never pip install packaging end up calling it indirectly.

Package metadata

  • Maintainer: Python Packaging Authority (pypa)
  • Project home: github.com/pypa/packaging
  • Docs: packaging.pypa.io
  • PyPI: pypi.org/project/packaging
  • License: BSD-2-Clause OR Apache-2.0
  • Governance: Python Packaging Authority
  • First released: 2014
  • Downloads: consistently in PyPI top 5 (transitive through pip / setuptools / etc.)

Optional dependencies & extras

  • None. packaging is a pure-Python wheel with no runtime dependencies.

Alternatives

PackageTrade-off
pkg_resources (deprecated, from setuptools)The historical "version comparator". Slow, monolithic, deprecated by PyPA. Avoid in new code.
semverStrict semver-only — does not handle Python's 1.2.3.dev4+local extensions. Use for projects that follow semver exactly.
parverModeled on PEP 440 with a mutable API. Niche; useful for build tooling that needs to bump versions.
Hand-rolled regexAlways wrong eventually. Don't.

Common gotchas

  1. Version is the canonical comparatorVersion("1.10") > Version("1.9") is True (lexically it would be False). Always use Version, never string compare.
  2. LegacyVersion is gone. Removed in 22.0. Strings that don't match PEP 440 now raise InvalidVersion. Old code that relied on legacy behavior needs explicit handling.
  3. Pre-releases are excluded from SpecifierSet matches by default. SpecifierSet(">=1.0").contains("1.0a1") is False unless you pass prereleases=True.
  4. Requirement parses one requirement string (e.g. "pip>=23.0; python_version>='3.10'"); it does NOT read a full requirements.txt (no support for -r, -e, comments).
  5. canonicalize_name() lowercases AND replaces underscores/dots with hyphens. This matters for PyPI matching — Pillow and pillow and PIL_LOW all canonicalize to pillow.
  6. Environment markers need a marker environment. Marker("python_version >= '3.10'").evaluate() uses the current interpreter's environment unless you pass a dict.
  7. Specifier (singular) vs SpecifierSet. A Specifier is one clause (>=1.0); a SpecifierSet is ,-joined (>=1.0,<2.0). Most user-facing strings produce a SpecifierSet.

Real-world recipes

The recipes cover the core building blocks: parse a version, compare versions, match a specifier, parse a requirement, canonicalize a name.

Recipe 1 — Parse and inspect a Version.

python
from packaging.version import Version

v = Version("2.4.0rc1.post3.dev2+local.1")
print(v.major, v.minor, v.micro)
print(v.is_prerelease, v.is_devrelease, v.is_postrelease)
print(v.local, v.epoch)

Output:

sql
2 4 0
True True True
local.1 0

Version exposes every PEP 440 field as an attribute.

Recipe 2 — Compare versions semantically.

python
from packaging.version import Version

print(Version("1.10") > Version("1.9"))         # True — numeric, not lexical
print(Version("2.0.0") > Version("2.0.0rc1"))   # True — release > prerelease
print(Version("1.0.0") == Version("1.0"))       # True — trailing zero ignored
print(sorted(["1.10", "1.9", "1.10rc1"], key=Version))

Output:

python
True
True
True
['1.9', '1.10rc1', '1.10']

sorted(..., key=Version) is the idiomatic way to sort a version list.

Recipe 3 — Match a SpecifierSet against installed versions.

python
from packaging.specifiers import SpecifierSet
from packaging.version import Version

spec = SpecifierSet(">=1.20,<2.0,!=1.21.0")
candidates = [Version(v) for v in ["1.19.5", "1.20.0", "1.21.0", "1.22.0", "2.0.0"]]
print([str(v) for v in candidates if v in spec])

Output:

css
['1.20.0', '1.22.0']

in returns whether the version satisfies the set. For pre-release semantics, construct SpecifierSet(..., prereleases=True).

Recipe 4 — Parse a PEP 508 Requirement.

python
from packaging.requirements import Requirement

req = Requirement('pip>=23.0; python_version >= "3.10" and sys_platform != "win32"')
print(req.name, req.specifier, req.marker)
print(req.marker.evaluate())   # whether the marker matches the current environment

Output:

graphql
pip >=23.0 python_version >= "3.10" and sys_platform != "win32"
True

Requirement covers name, optional extras, version specifier, and marker — exactly what pip install parses.

Recipe 5 — Canonicalize package names.

python
from packaging.utils import canonicalize_name

names = ["Pillow", "pillow", "PIL_LOW", "pi-llow", "Pi.LLow"]
print(set(canonicalize_name(n) for n in names))

Output:

arduino
{'pi-llow', 'pillow'}

Note: only names that already differ in word boundaries collapse together; Pillow and PIL_LOW both canonicalize to pillow.

Recipe 6 — Pick the highest compatible version.

python
from packaging.specifiers import SpecifierSet
from packaging.version import Version

available = ["3.0.0", "3.5.2", "3.5.3", "3.6.0", "4.0.0"]
spec = SpecifierSet("~=3.5")    # PEP 440 compatible release: >=3.5, <4
ok = [Version(v) for v in available if Version(v) in spec]
print(max(ok))

Output:

code
3.6.0

The "compatible release" operator (~=) excludes the next major (4.0).

Performance tuning

  • packaging is fast. Parsing a Version is sub-microsecond; matching a SpecifierSet is single-digit microseconds.
  • Cache Version and SpecifierSet instances when scanning thousands of requirements — construction is the bulk of the cost.
  • canonicalize_name is cheap; still, cache it for big dependency graphs.

Version migration guide

  • 21.x → 22.0LegacyVersion removed; strings that don't parse to PEP 440 now raise InvalidVersion. Old codebases relying on legacy comparisons need updating.
  • 22.x → 23.0 — minimum Python 3.7; Specifier constructor stricter about whitespace.
  • 23.x → 24.0 — minimum Python 3.8; updated marker grammar; Tag API stabilised.
  • 24.x → 25.x — PEP 668 (externally-managed environments) markers honored by tooling; library API stable.
python
# Pre-22.0
from packaging.version import LegacyVersion       # removed
# Modern
from packaging.version import InvalidVersion, Version
try:
    v = Version("1.x.dev")
except InvalidVersion:
    v = None

Output: modern code fails explicitly on non-PEP 440 input; reject or normalize at the boundary.

Security considerations

  • Untrusted version strings can be very long; Version has an internal regex but treat malicious-length strings as a DoS surface — cap input length.
  • Requirement does NOT execute markers as code — markers are evaluated against a constrained AST. Still, do not pass attacker-controlled markers to Marker.evaluate() unless you understand what variables it reads.
  • canonicalize_name is the only safe way to compare package names — use it before authorization decisions about packages (e.g. "is this an allow-listed dependency?"). Naive == on raw names misses case variants.

Testing & CI

python
from packaging.specifiers import SpecifierSet
from packaging.version import Version

def test_specifier_matches():
    assert Version("1.5") in SpecifierSet(">=1.0,<2.0")
    assert Version("2.0") not in SpecifierSet(">=1.0,<2.0")
    assert Version("1.0rc1") not in SpecifierSet(">=1.0")     # default
    assert Version("1.0rc1") in SpecifierSet(">=1.0", prereleases=True)

Output: all assertions hold; useful sanity check in any project that exposes a "find compatible version" feature.

Ecosystem integrations

  • pip — vendors packaging internally; uses it for every requirement parse and dependency resolution step.
  • setuptools — wraps packaging for setup.py/setup.cfg/pyproject.toml metadata.
  • build — used to validate pyproject.toml and produce wheels with PEP 425 tags.
  • poetry, uv, hatch — all use packaging (or compatible parsers) for version/specifier handling.
  • pip-audit, pip-tools — consume packaging for requirement parsing.

Compatibility matrix

PythonpackagingNotes
3.621.x (frozen)Final supported line for 3.6.
3.723.xFinal supported line for 3.7.
3.824.x+Lowest current floor.
3.924.x+Stable.
3.1024.x+Stable.
3.1124.x+Stable.
3.1224.x+Stable.
3.1324.x+Stable; wheels universal.

Production deployment

  • Pin a minimum version in libraries that touch versioning logic (packaging>=24). The PyPA occasionally tightens parsers; old packaging may silently accept malformed input.
  • Never use pip._vendor.packaging in your own code — that's a private vendored copy and pip can change or remove it.
  • Validate input at the boundary. Wrap Version() and Requirement() in try/except and surface InvalidVersion / InvalidRequirement as user-facing errors.
  • For long-running services that index package metadata, cache Version and SpecifierSet instances by string — they're hashable and immutable.

When NOT to use this

  • Strict semver ecosystems — use semver if you need 2.0.0-rc.1 semantics exactly.
  • Generic version comparison outside the Python ecosystem — packaging understands PEP 440 quirks (post, dev, epochs) that are non-standard elsewhere.
  • Build-system metadata generation — use hatch, setuptools, or flit directly; packaging is the consumer layer.
  • Reading full requirements.txtpackaging parses one requirement line at a time. Use pip-requirements-parser for the full grammar (comments, -r, -e, hashes).

Troubleshooting common errors

Error / SymptomLikely causeFix
InvalidVersion: '1.x.dev'Non-PEP 440 stringNormalize at the boundary; reject invalid input.
InvalidSpecifier: '>1.0 and <2.0'Word and not allowedUse comma: ">1.0,<2.0".
InvalidRequirement: ...Stray whitespace or extras formattingStrip input; ensure name[extra1,extra2]>=1.0 shape.
SpecifierSet.contains("1.0rc1") returns FalsePre-releases excluded by defaultPass prereleases=True.
LegacyVersion import errorRemoved in 22.0Catch InvalidVersion and handle explicitly.
Marker.evaluate() returns False unexpectedlyWrong environment dictInspect default_environment() and compare.

See also