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
pip install packaging
Output: (none — exits 0 on success; pure-Python, zero runtime dependencies)
uv add packaging
Output: dependency resolved + added to pyproject.toml
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
packagingversions. - The library is bundled inside
pip(vendored copy atpip._vendor.packaging), so even apps that neverpip install packagingend 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.
packagingis a pure-Python wheel with no runtime dependencies.
Alternatives
| Package | Trade-off |
|---|---|
pkg_resources (deprecated, from setuptools) | The historical "version comparator". Slow, monolithic, deprecated by PyPA. Avoid in new code. |
semver | Strict semver-only — does not handle Python's 1.2.3.dev4+local extensions. Use for projects that follow semver exactly. |
parver | Modeled on PEP 440 with a mutable API. Niche; useful for build tooling that needs to bump versions. |
| Hand-rolled regex | Always wrong eventually. Don't. |
Common gotchas
Versionis the canonical comparator —Version("1.10") > Version("1.9")is True (lexically it would be False). Always useVersion, never string compare.LegacyVersionis gone. Removed in22.0. Strings that don't match PEP 440 now raiseInvalidVersion. Old code that relied on legacy behavior needs explicit handling.- Pre-releases are excluded from
SpecifierSetmatches by default.SpecifierSet(">=1.0").contains("1.0a1")is False unless you passprereleases=True. Requirementparses one requirement string (e.g."pip>=23.0; python_version>='3.10'"); it does NOT read a fullrequirements.txt(no support for-r,-e, comments).canonicalize_name()lowercases AND replaces underscores/dots with hyphens. This matters for PyPI matching —PillowandpillowandPIL_LOWall canonicalize topillow.- Environment markers need a marker environment.
Marker("python_version >= '3.10'").evaluate()uses the current interpreter's environment unless you pass a dict. Specifier(singular) vsSpecifierSet. ASpecifieris one clause (>=1.0); aSpecifierSetis,-joined (>=1.0,<2.0). Most user-facing strings produce aSpecifierSet.
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.
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:
2 4 0
True True True
local.1 0
Version exposes every PEP 440 field as an attribute.
Recipe 2 — Compare versions semantically.
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:
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.
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:
['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.
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:
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.
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:
{'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.
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:
3.6.0
The "compatible release" operator (~=) excludes the next major (4.0).
Performance tuning
packagingis fast. Parsing a Version is sub-microsecond; matching a SpecifierSet is single-digit microseconds.- Cache
VersionandSpecifierSetinstances when scanning thousands of requirements — construction is the bulk of the cost. canonicalize_nameis cheap; still, cache it for big dependency graphs.
Version migration guide
21.x → 22.0—LegacyVersionremoved; strings that don't parse to PEP 440 now raiseInvalidVersion. Old codebases relying on legacy comparisons need updating.22.x → 23.0— minimum Python 3.7;Specifierconstructor stricter about whitespace.23.x → 24.0— minimum Python 3.8; updated marker grammar;TagAPI stabilised.24.x → 25.x— PEP 668 (externally-managed environments) markers honored by tooling; library API stable.
# 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;
Versionhas an internal regex but treat malicious-length strings as a DoS surface — cap input length. Requirementdoes NOT execute markers as code — markers are evaluated against a constrained AST. Still, do not pass attacker-controlled markers toMarker.evaluate()unless you understand what variables it reads.canonicalize_nameis 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
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— vendorspackaginginternally; uses it for every requirement parse and dependency resolution step.setuptools— wrapspackagingforsetup.py/setup.cfg/pyproject.tomlmetadata.build— used to validatepyproject.tomland produce wheels with PEP 425 tags.poetry,uv,hatch— all usepackaging(or compatible parsers) for version/specifier handling.pip-audit,pip-tools— consumepackagingfor requirement parsing.
Compatibility matrix
| Python | packaging | Notes |
|---|---|---|
| 3.6 | 21.x (frozen) | Final supported line for 3.6. |
| 3.7 | 23.x | Final supported line for 3.7. |
| 3.8 | 24.x+ | Lowest current floor. |
| 3.9 | 24.x+ | Stable. |
| 3.10 | 24.x+ | Stable. |
| 3.11 | 24.x+ | Stable. |
| 3.12 | 24.x+ | Stable. |
| 3.13 | 24.x+ | Stable; wheels universal. |
Production deployment
- Pin a minimum version in libraries that touch versioning logic (
packaging>=24). The PyPA occasionally tightens parsers; oldpackagingmay silently accept malformed input. - Never use
pip._vendor.packagingin your own code — that's a private vendored copy and pip can change or remove it. - Validate input at the boundary. Wrap
Version()andRequirement()intry/exceptand surfaceInvalidVersion/InvalidRequirementas user-facing errors. - For long-running services that index package metadata, cache
VersionandSpecifierSetinstances by string — they're hashable and immutable.
When NOT to use this
- Strict semver ecosystems — use
semverif you need 2.0.0-rc.1 semantics exactly. - Generic version comparison outside the Python ecosystem —
packagingunderstands PEP 440 quirks (post,dev, epochs) that are non-standard elsewhere. - Build-system metadata generation — use
hatch,setuptools, orflitdirectly;packagingis the consumer layer. - Reading full
requirements.txt—packagingparses one requirement line at a time. Usepip-requirements-parserfor the full grammar (comments,-r,-e, hashes).
Troubleshooting common errors
| Error / Symptom | Likely cause | Fix |
|---|---|---|
InvalidVersion: '1.x.dev' | Non-PEP 440 string | Normalize at the boundary; reject invalid input. |
InvalidSpecifier: '>1.0 and <2.0' | Word and not allowed | Use comma: ">1.0,<2.0". |
InvalidRequirement: ... | Stray whitespace or extras formatting | Strip input; ensure name[extra1,extra2]>=1.0 shape. |
SpecifierSet.contains("1.0rc1") returns False | Pre-releases excluded by default | Pass prereleases=True. |
LegacyVersion import error | Removed in 22.0 | Catch InvalidVersion and handle explicitly. |
Marker.evaluate() returns False unexpectedly | Wrong environment dict | Inspect default_environment() and compare. |
See also
- Python: pip — pip's wire format
- Python: pyproject-toml — modern build metadata
- Official packaging docs
- PEP 440 — Version Identification