Check if an IP address is valid in Python
Python's `ipaddress` module validates and classifies any address for free — here's exactly how far it gets you, and the line parsing can't cross.
Contents
You want to validate an IP address in Python, and you almost certainly do not need a package, a service, or a regex you copied off a forum in 2013. The standard library ships a module that parses IPv4 and IPv6 correctly and tells you what kind of address you're holding. Start there. This post shows exactly how far ipaddress gets you — which is further than most people expect — and where a hosted API is actually adding something instead of reselling what your interpreter already does.
The stdlib already does this: ipaddress.ip_address
ipaddress.ip_address(s) takes a string, returns an IPv4Address or IPv6Address, and raises ValueError if the string isn't a real address. That exception is your validity check — no regex, no length math, no guessing about IPv6 :: compression.
import ipaddress
def is_valid_ip(value: str) -> bool:
try:
ipaddress.ip_address(value)
return True
except ValueError:
return False
print(is_valid_ip("8.8.8.8")) # True
print(is_valid_ip("2606:4700::1111")) # True
print(is_valid_ip("999.1.1.1")) # False (octet out of range)
print(is_valid_ip("192.168.1")) # False (not four octets)
print(is_valid_ip("not-an-ip")) # False
That's the whole answer to "is this a valid IP." A regex would happily pass 999.1.1.1; ipaddress rejects it because it parses the octets, not just the shape. The .version attribute tells you which family you got:
ip = ipaddress.ip_address("2606:4700::1111")
print(ip.version) # 6
Available since Python 3.3, no install, no dependency to audit.
Classify the address, not just validate it
Valid isn't the same as useful. 203.0.113.9 is a valid IPv4 address and also a documentation range you should never see in production traffic. The parsed object carries boolean properties that answer the questions you actually have:
import ipaddress
def classify(value: str) -> dict:
ip = ipaddress.ip_address(value) # raises ValueError if invalid
return {
"value": str(ip),
"version": ip.version,
"is_private": ip.is_private,
"is_global": ip.is_global,
"is_loopback": ip.is_loopback,
"is_link_local": ip.is_link_local,
"is_reserved": ip.is_reserved,
"is_multicast": ip.is_multicast,
}
print(classify("10.0.0.1"))
# {'value': '10.0.0.1', 'version': 4, 'is_private': True,
# 'is_global': False, 'is_loopback': False, ...}
print(classify("127.0.0.1")["is_loopback"]) # True
print(classify("8.8.8.8")["is_global"]) # True
is_private covers the RFC 1918 ranges plus other non-routable space, is_global tells you it's a publicly routable address, and the rest name the special cases: loopback, link-local (169.254.x / fe80::), multicast, and reserved. If you want the reasoning behind those buckets, what counts as a private IP walks through the ranges. This is real classification, computed locally, for zero cost.
Normalize while you're at it
Two strings can name the same address. Parsing collapses them to one canonical form, which is what you want before storing an IP or comparing two of them:
import ipaddress
print(str(ipaddress.ip_address("2001:0db8:0000:0000:0000:0000:0000:0001")))
# 2001:db8::1
So the stdlib gives you the three things "IP validation" usually means: is it valid, what version, and what kind of address — plus a normalized string. Deduplicate your storage against the normalized value, not the raw input.
When the stdlib is genuinely all you need
If your job is rejecting garbage in a form, blocking private ranges from a webhook target, or sorting inbound addresses into routable vs. not, stop reading and use ipaddress. It's correct, it's free, it has no network dependency, and it can't be down. Doing the same work in JavaScript? The Node version is validate an IP address in Node.js — same idea, different stdlib.
Reach for a service only when the value it adds is a thing your language cannot compute from the address bytes.
Where an API earns its place
Boundstone's /v1/verify/ip runs the exact classification above — same public/private/bogon logic — but over HTTP, and as one contract that also covers phone and email. The point isn't that it validates IPs better than your interpreter. It's that one client, one response shape, and one honesty contract span all three. The is_bogon flag is the piece the stdlib doesn't hand you directly (you'd derive it from is_private, is_reserved, and friends).
import requests
resp = requests.post(
"https://api.boundstone.io/v1/verify/ip",
headers={"Authorization": "Bearer bs_live_YOUR_KEY"},
json={"ip": "8.8.8.8"},
)
data = resp.json()
print(data["valid"], data["version"], data["classification"])
print(data["is_public"], data["is_bogon"])
print("checked: ", data["checks"]["performed"])
print("not checked:", data["checks"]["not_performed"])
# checked: ['format', 'version', 'range_classification']
# not checked: ['geolocation', 'asn', 'hosting_datacenter', 'proxy_vpn_tor', 'reputation']
Read checks.not_performed and take it literally. Boundstone does not do IP geolocation, ASN lookup, hosting/datacenter detection, proxy/VPN/Tor detection, or reputation scoring. That intelligence layer needs licensed data and isn't shipped — for anyone, on any plan. When a response says valid, it means format, version, and range classification checked out, and nothing more is being implied. That named list of what we didn't do is the reason to trust the small list of what we did. If you want to see it without a key, the free IP validator tool runs the same classification in the browser.
The short version
- Validating an IP in Python?
ipaddress.ip_address(s)— it raisesValueErroron anything invalid. That's the answer. - Need version or classification? Read
.version,.is_private,.is_global,.is_loopback,.is_reserved,.is_multicast. Free, local, correct. - Storing addresses? Normalize with
str(ip)first. - Want it over HTTP alongside phone and email checks? That's the API — same classification, one contract, 250 credits a month free, no card, credits never expire.
- Want geo, VPN detection, or reputation? No parser computes that from the address bytes — it takes licensed data, which Boundstone doesn't ship today. Anyone claiming a free "valid" also proves the address is a real user is selling you something the bytes can't support.