I thought of a potential solution that moves the handling of this problem to the point where addresses/networks/etc are defined rather than every time is_global and is_private are queried.
Network range semantics
We want to define 3 different sets of potential network range categorisation semantics for IPv6 address and network instances to follow:
universal: the current behaviour, where both is_private and is_global are False for the carrier private address range
carrier: the carrier private range reports addr.is_private == True (addr.is_global remains False)
endpoint: the carrier private range reports addr.is_global == True (addr.is_private remains False)
The first case will ideally be transitional - the eventual default should be endpoint semantics, with the ability to explicitly request carrier semantics.
To allow for deprecation of the universal case, we'd need to allow carrier or endpoint semantics to be explicitly requested:
ipaddress.set_default_network_range_semantics(carrier=True): makes carrier semantics the default
ipaddress.set_default_network_range_semantics(carrier=False): makes endpoint semantics the default
Which implies a public API to query the default semantics as well:
ipaddress.get_default_network_range_semantics(): returns a string value ("universal" if no default semantics have been set, "carrier" or "endpoint" as appropriate if they have)
Defining a real enum seems like overkill here (ipaddress currently only depends on functools and nothing else, so bringing in 2000 lines of enum support code isn't a trivial change, especially for an obscure issue like this one), but a tri-state boolean would be too cryptic as a status reporting API (even though it's reasonably clear when setting the value) , so returning string names seems like a decent compromise.
IPv6 address and network changes
IPv6 address and network instances would take a new tri-state boolean carrier option (where the default None means "use the module level default").
That option (and potentially the module default setting) would control how is_private and is_global behave on that instance.
They would gain a new string property range_semantics that would report "universal"/"carrier"/"endpoint" as appropriate.
Defaulting to "endpoint" semantics
We don't actually know if telcos and other backbone operators are relying on the current is_global and is_private behaviour to identify addresses in the shared address space. This means the conservative option for getting to endpoint semantics as the default behaviour would be to:
- emit
DeprecationWarning if an address or network in the carrier private range is defined and no default network semantics have been set (with directions to call set_default_semantics() or specify carrier to make the warning go away)
- eventually drop the deprecation warning, get rid of the "unknown" default semantics, and make endpoint semantics the default behaviour
Alternatively, we could decide that endpoint use cases so massively outweigh carrier use cases that it's OK to skip the deprecation period, and instead just add a porting note that carriers relying on is_global being False for carrier-private addresses will need to call ipaddress.set_default_semantics(carrier=True) in their applications (which will also make is_private return True).
Addresses in the private range could still be specifically identified with that default behaviour, but you'd have to create two address instances (one with carrier range semantics, one with endpoint semantics) and see if they reported different is_global or is_private values.
We'd need SC approval for that (as per PEP 387), and I'm not sure they'd grant it (we can reasonably issue a deprecation notice, it's just highly arguable how useful it would be to do so when we're pretty sure 99.9% of the userbase will want the new behaviour and would see it as a bug fix rather than as functional change). A straw poll on discuss.python.org might be helpful to get a sense of whether folks think it's OK to spend a couple of years getting away from the currently ambiguous default range handling semantics.
Edit: changed the suggested name of the current default semantics from "unknown" to "universal" ("unknown" made it sound like the range semantics were unknown, which isn't the case. Calling that case "universal range semantics" reflects the fact that is_global and is_private are only true when they're true under both "carrier" and "endpoint" semantics)
Edit 2: I realised what this case was reminding me of: the fold flag in the datetime module where the need to disambiguate the edge case of the overlapping hour when time zones switch out of daylight saving ends up needing to be tracked through the entire data model. Here, the network range semantics for endpoint and carrier use cases are almost identical... except in one particular range, where they're precise mirrors of each other. At least this case has the benefit that setting an application wide default is likely to be a reasonable approach (unlike timezone DST handling) (see next comment).
Edit 3: An even simpler third option would be to never change the default. carrier=None would request "universal" semantics, while any other false value would request "endpoint" semantics, and the current "universal" behaviour would remain the default indefinitely. I didn't like that idea with my original name for the "universal" semantics, but I'm more amenable to it now they have a reasonable name for the query APIs to return. Assuming we do end up using this "explicit range semantics" design, changing the default semantics should be split out into a follow-up issue, rather than being part of the same issue that adds the ability to explicitly request "carrier" or "endpoint" semantics.