ipaddress: is_private and is_global Are Not Always Opposites (tested)
It's tempting to treat is_private and is_global as a simple either/or -- if an address isn't private, it must be globally routable, and vice versa. We measured both properties across a normal public address, a normal private address, and the carrier-grade NAT shared address space, to see whether that assumption holds.
1. For ordinary addresses, the two properties are opposites
import ipaddress
for s in ("8.8.8.8", "192.168.1.1", "10.0.0.5"):
a = ipaddress.ip_address(s)
print(s, a.is_private, a.is_global)
8.8.8.8 False True
192.168.1.1 True False
10.0.0.5 True False
Measured: for a normal public address and two normal private (RFC 1918) addresses, is_private and is_global are always exact opposites -- exactly True in one and False in the other. This is the pattern that makes not ip.is_private feel like a safe stand-in for "is this address globally routable."
2. CGNAT shared address space is neither private nor global
import ipaddress
a = ipaddress.ip_address("100.64.0.5")
print(a.is_private, a.is_global)
False False
Measured: 100.64.0.5 falls in 100.64.0.0/10, the carrier-grade NAT shared address space defined by RFC 6598 for ISPs to use between customer routers and their own NAT, without consuming public IPv4 space. ipaddress correctly does not count it as is_private (it's not meant for a single private network like 192.168.0.0/16), but it also correctly does not count it as is_global (it's not meant to be routed on the public internet either). The two properties aren't opposites for this range -- both are simply False.
3. The fix: never derive one flag from the other -- check is_global directly
import ipaddress
candidates = ["8.8.8.8", "192.168.1.1", "100.64.0.5"]
for s in candidates:
a = ipaddress.ip_address(s)
treat_as_public_WRONG = not a.is_private
treat_as_public_RIGHT = a.is_global
print(s, "wrong:", treat_as_public_WRONG, "right:", treat_as_public_RIGHT)
8.8.8.8 wrong: True right: True
192.168.1.1 wrong: False right: False
100.64.0.5 wrong: True right: False
Measured: code that checks not ip.is_private to decide whether an address is safe to treat as public/routable gets the CGNAT address wrong -- it's reported as "not private" and therefore treated as public, when is_global correctly says it isn't globally routable at all. If the goal is "is this address reachable from the public internet," check is_global directly rather than inferring it from the negation of is_private.
Frequently asked questions
Are is_private and is_global always opposite values in Python's ipaddress module?
No -- we measured ipaddress.ip_address('100.64.0.5').is_private and .is_global both returning False for an address in the CGNAT shared address space (100.64.0.0/10, RFC 6598), which is neither private nor globally routable.
Is it safe to use 'not ip.is_private' to check if an IP address is public?
Not always -- we measured this inference giving the wrong answer for CGNAT shared-space addresses: not is_private is True (suggesting public), but is_global is correctly False. Check is_global directly instead of negating is_private.
What is the 100.64.0.0/10 address range in Python's ipaddress module?
It's the carrier-grade NAT (CGNAT) shared address space defined by RFC 6598, used by ISPs between customer equipment and their own NAT layer -- we measured ipaddress correctly classifying it as neither is_private nor is_global.
For normal public and private IP addresses, do is_private and is_global agree?
Yes -- we measured both properties behaving as exact opposites for ordinary addresses like 8.8.8.8 (public) and 192.168.1.1 / 10.0.0.5 (private, RFC 1918); the mismatch only appears for special ranges like CGNAT space.
How should I check if an IP address is reachable from the public internet in Python?
Use the is_global property directly rather than inferring it from is_private -- we confirmed is_global correctly returns False for CGNAT addresses while a not is_private check would incorrectly suggest they're public.
Written with AI assistance. Measured on 2026-10-04 with Python 3.13.5's built-in ipaddress module: checked is_private and is_global on a normal public address, a normal private address, and a CGNAT shared-address-space address (100.64.0.0/10, RFC 6598), to see whether the two properties are always exact opposites of each other. PlainHeaders is our extension; Python's ipaddress module is built in.