An IPv6 Zone ID Breaks Equality and Set Membership in ipaddress (tested)
Link-local IPv6 addresses can carry a zone ID suffix (%eth0, %1) identifying which network interface they're scoped to -- the kind of value you get back from socket.getnameinfo() or OS networking tools. We measured whether Python's ipaddress module treats a zone-tagged address as the same host as the identical address without one.
1. ipaddress parses the zone ID without error, and is_link_local still works
import ipaddress
zoned = ipaddress.ip_address("fe80::1%eth0")
print(zoned, zoned.scope_id)
print(zoned.is_link_local)
fe80::1%eth0 eth0
True
Measured: ipaddress.ip_address() accepts the %eth0 zone suffix with no error, exposes it via .scope_id, and per-address properties like is_link_local correctly ignore the zone ID when classifying the address.
2. But it is never equal to the same address without a zone ID
import ipaddress
zoned = ipaddress.ip_address("fe80::1%eth0")
plain = ipaddress.ip_address("fe80::1")
print(zoned == plain)
print(hash(zoned) == hash(plain))
blocklist = {plain}
print(zoned in blocklist)
False
False
False
Measured: an address with a zone ID and the same address without one are never ==, hash differently, and fail set membership against each other -- even though they identify the same link-local host. Any code that collects link-local addresses from one source (say, a zone-tagged value from a live socket) and compares against a blocklist or cache built from zone-less strings will silently miss matches, exactly the same shape as comparing plain IPv4Address and IPv6Address objects.
3. The zone ID also silently vanishes when building a network
import ipaddress
net = ipaddress.ip_network("fe80::1%eth0/64", strict=False)
print(net)
fe80::/64
Measured: ip_network() accepts the zone-tagged address and computes the network correctly, but the zone ID is dropped entirely from the result -- there is no error or warning that scope information was discarded, so code that later expects the network object to retain which interface the original address was scoped to will find nothing there. If you need to compare or deduplicate link-local addresses that may or may not carry a zone ID, strip or normalize .scope_id explicitly before comparing, rather than relying on == or set membership to see through it.
Frequently asked questions
Does Python's ipaddress module treat an IPv6 zone ID as part of address equality?
Yes -- we measured ipaddress.ip_address('fe80::1%eth0') == ipaddress.ip_address('fe80::1') returning False, even though both represent the same link-local host and is_link_local is True for both.
Can a zone-tagged IPv6 address bypass a set-based blocklist in Python?
Yes, if the blocklist was built from addresses without zone IDs -- we measured a zone-tagged address failing 'in' membership against a set containing the identical address minus the zone ID, with no error or warning.
Does ipaddress.ip_network keep the zone ID from an IPv6 zone-tagged address?
No -- we measured ip_network('fe80::1%eth0/64', strict=False) silently dropping the %eth0 zone ID from the resulting network object, with no indication the scope information was discarded.
How do I compare IPv6 addresses that may or may not have a zone ID?
Strip or normalize the .scope_id attribute explicitly before comparing or hashing -- we confirmed Python's ipaddress module does not do this automatically, and == / hash() / set membership all treat zone-tagged and zone-less forms as entirely different values.
What is scope_id on an ipaddress IPv6Address object?
It holds the zone ID suffix (e.g. 'eth0' from fe80::1%eth0) when present, or None otherwise -- we measured it populated correctly for a parsed zone-tagged address while per-address classification properties like is_link_local still work normally.
Written with AI assistance. Measured on 2026-10-04 with Python 3.13.5's built-in ipaddress module: parsed the same link-local address with and without an IPv6 zone ID ('fe80::1%eth0' vs 'fe80::1'), compared them with ==, hash(), and set membership, and checked whether is_link_local still worked on the zone-tagged form. PlainHeaders is our extension; Python's ipaddress module is built in.