Skip to content

IPv6Address ordering is scope-id-blind, breaking trichotomy and making sorted()/min()/max() non-deterministic for scoped addresses #151769

@Builder106

Description

@Builder106

Bug report

Bug description:

COMPONENT: Library (Lib/ipaddress.py)

AFFECTED VERSIONS: main (3.16.0a0) and 3.14 (reproduced on 3.14.6). The defect dates to when scope_id was added to IPv6Address.__eq__/__hash__/__str__ but not to ordering.

SUMMARY

IPv6Address.__eq__ and __hash__ fold in the interface scope_id (so fe80::1 and fe80::1%eth0 are unequal and hash differently), but IPv6Address does not override ordering — it inherits _BaseAddress.__lt__, which compares only the integer address and ignores _scope_id. _BaseAddress is decorated with @functools.total_ordering, which synthesizes __gt__/__le__/__ge__ from __lt__ and __eq__. The synthesized __gt__ is effectively not (a < b) and a != b. For two addresses with the same integer but different scope, a < b is False (scope-blind __lt__) while a != b is True (scope-aware __eq__), so a > b is True — and symmetrically b > a is also True. The relation is not a strict order: trichotomy and antisymmetry both fail, and sorting is non-deterministic.

MINIMAL REPRODUCTION

>>> import ipaddress
>>> a = ipaddress.ip_address('fe80::1')
>>> b = ipaddress.ip_address('fe80::1%eth0')
>>> a == b
False
>>> a > b
True
>>> b > a            # antisymmetry violated: both directions are "greater"
True

>>> from itertools import permutations
>>> items = [ipaddress.ip_address(s)
...          for s in ('fe80::1', 'fe80::1%eth0', 'fe80::1%eth1')]
>>> {tuple(str(x) for x in sorted(p)) for p in permutations(items)}
# 6 DISTINCT outputs — sorted() just returns the input order

ROOT CAUSE

scope_id was added to IPv6Address.__eq__/__hash__/__str__ but not to ordering, and IPv6Address defines no __lt__. The @functools.total_ordering operators on _BaseAddress dispatch through type(self).__lt__, which (absent an override) resolves to the scope-blind _BaseAddress.__lt__ while total_ordering also consults the scope-aware __eq__ — so they disagree. The functools docs note total_ordering "makes no attempt to override methods declared in the class or its superclasses," which is exactly why the subclass inherits a __lt__ inconsistent with its own __eq__.

The minimal fix is a scope-aware IPv6Address.__lt__ (tie-break on scope_id only when the integer address is equal; unscoped sorts before scoped; scope ids compare lexicographically). Because the derived operators dispatch through type(self).__lt__, overriding only __lt__ on the subclass routes all four comparisons through the scope-aware path. This also transitively repairs scoped IPv6Interface and IPv6Network ordering, which delegate to address comparison. _BaseAddress.__lt__ is untouched, so IPv4Address is unaffected.

RELATION TO CONCURRENT WORK (#141647 / PR #141842) — please don't close as a dup

There is concurrent activity in gh-141647 / PR gh-141842 touching the comparison dunders, but it is orthogonal: that PR's rewritten _BaseAddress.__lt__ stays scope-blind (return self._ip < other._ip) and adds no IPv6Address override, and its test_mixed_type_ordering skips same-type pairs, so this same-type scoped trichotomy failure survives that PR untested. This fix adds a scope-aware override on the IPv6Address subclass only and does not modify _BaseAddress.__lt__, so it composes cleanly regardless of merge order (or could be folded into #141842).

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.14bugs and security fixes3.15pre-release feature fixes, bugs and security fixes3.16new features, bugs and security fixesstdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error
    No fields configured for issues without a type.

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions