# Check if an IP address is valid in Python

_2026-07-27 · Boundstone (https://boundstone.io/blog/validate-ip-python)_


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.

```python
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:

```python
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:

```python
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](/blog/what-is-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:

```python
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](/blog/validate-ip-nodejs) — 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).

```python
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](/tools/ip-validator) runs the same classification in the browser.

## The short version

- **Validating an IP in Python?** `ipaddress.ip_address(s)` — it raises `ValueError` on 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.
