# What changed in packaging

> 75 changes in packaging 26 from its official release notes, upgrade guides and advisories: 23 breaking, 21 new APIs and options. Each one cited. Sources checked 2026-10-18. HTML: https://unstale.sh/pypi/packaging/

To see which of these apply to a repository, run `npx @unstale/cli init` in it.

## packaging 26

### Breaking changes

- Malformed Description-Content-Type values are now rejected. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Local directories in direct_url now require absolute file URLs. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Misplaced WITH clauses and empty LicenseRef- names now raise InvalidLicenseExpression. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Names containing collapsed double hyphens, such as a--b, are now rejected. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Linux tag generation now prefers native linux_* tags over manylinux and musllinux tags. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Serialization now retains parentheses around nested marker groups. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Tags with the wrong number of components now raise InvalidTag. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Empty tag components are now rejected. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- TOML booleans are now rejected where integers are expected. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Python 3.8 is no longer supported; packaging requires Python 3.9 or later. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- An invalid specifier inside a requirement now raises InvalidRequirement instead of InvalidSpecifier. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Requirements and markers ending in a line break are now rejected. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Set-valued variables such as extras now raise UndefinedComparison when used outside the membership form. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Marker evaluation raises this KeyError subclass when an environment key is missing. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Passing a non-string to Version now raises InvalidVersion instead of TypeError. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Non-string pre-release letters now raise InvalidVersion. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Wheel filenames ending in a newline are now rejected. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Wheel tags with a non-identifier interpreter component are now rejected. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- The helper is renamed to _format_full_version to make it visibly private. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- ValueError from int-max-str-digits is now propagated. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Local version identifiers are now restricted to ASCII. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Pylock now gives preference to a path over a URL. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- The === operator uses the original string when available instead of its normalized form. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)

### New APIs and options

- Adds a limit argument for parsing compressed tag sets. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Generates pure-Python tags for a Python version without consulting the running platform. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Adds a predicate to prefer source distributions over wheels for selected packages. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Compares the versions accepted by two specifier sets to determine whether they are disjoint. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Compares the versions accepted by two specifier sets to determine whether one is a subset of the other. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Compares the versions accepted by two specifier sets to determine whether one is a superset of the other. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Converts a SpecifierSet to a VersionRange representing the versions it accepts. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Adds an interval-set API for version ranges, with set operations, relations, membership tests, filtering, and conversion to a SpecifierSet where a PEP 440 form exists. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Converts a VersionRange back to a SpecifierSet when a PEP 440 form exists. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Adds a function for selecting compatible tags. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Marker objects can now be combined with the & and | operators. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Adds a filename property to PackageSdist. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Adds a filename property to PackageWheel. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Adds a module for working with dependency groups, based on the dependency-groups package. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Adds the packaging.direct_url module. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Adds the packaging.errors module. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Adds an option to validate the sort order of compressed tag sets when parsing wheel filenames. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Adds a select function for pylock. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Adds a key argument for filtering values with a SpecifierSet. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Adds a method that determines whether a SpecifierSet is unsatisfiable using ranges. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Adds Version.from_parts; Version.__replace__ also normalizes its result. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)

### Performance

- Complex SpecifierSet filtering is faster through cost-based ordering. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)

### Behavior and default changes

- Values in nested markers are normalized at parse time. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Requested extra names are normalized before requirements are compared or hashed. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- InvalidMetadata and CyclicDependencyGroup can now be pickled. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Metadata parsing now accepts Metadata-Version 2.6. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Specifier and SpecifierSet filtering now use the new range engine. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- The warn argument is now forwarded to generic_tags. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Wheel tag handling now recognizes the free-threading abi3.abi3t tag. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Pylock validates that names and versions are consistent with file names. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Specifiers are now canonicalized when deduplicating them in a SpecifierSet. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)

### Fixes and other changes

- Malformed dependency-group parses are no longer cached. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- InvalidRequirement errors are collected during dependency-group resolution instead of leaking. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Hashing internally trimmed versions no longer raises AttributeError. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Malformed string literal errors are wrapped in InvalidMarker or InvalidRequirement rather than leaking a low-level error. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Marker serialization preserves quoting semantics so serialized markers parse back to the same marker. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Empty tag components are now rejected. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- An explicitly empty tags sequence is used as-is instead of falling back to sys_tags(). (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- PylockSelectError is now included in packaging.pylock.__all__. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- A requirement's specifier prereleases override is preserved across a pickle round trip. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Hashing now agrees with equality for trailing-zero-equivalent specifiers, allowing equal requirements to deduplicate in sets and dictionaries. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Corrects unsatisfiability results for post-release boundary intersections. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- An empty project name is now rejected by both filename parsers. (26.3) — [release notes](https://github.com/pypa/packaging/releases/tag/26.3)
- Re-exports ExceptionGroup for now. (26.2) — [release notes](https://github.com/pypa/packaging/releases/tag/26.2)
- Version, Specifier, SpecifierSet, Tag, Marker, and Requirement are pickle-safe and backward-compatible with pickles created in 25.0–26.1, including references to the removed packaging._structures module. (26.2) — [release notes](https://github.com/pypa/packaging/releases/tag/26.2)
- Corrects the sysconfig variable name for pyemscripten. (26.2) — [release notes](https://github.com/pypa/packaging/releases/tag/26.2)
- Fixes incorrect self-comparison for InfinityType. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Fixes incorrect self-comparison for NegativeInfinityType. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Post-release exclusion for >V is narrowed to match the specification. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Pre-release exclusion for <V.postN is narrowed to match the specification. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Fixes handling of key when the specifier set is empty and prerelease is False. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
- Fixes > comparisons for versions with dev and local segments. (26.1) — [release notes](https://github.com/pypa/packaging/releases/tag/26.1)
