Guide · Python · SDK · REST API

IP Address Validation in Python — IPv4 & IPv6 Type Detection

IP addresses look deceptively simple but come in two vastly different formats with dozens of reserved ranges. Here's why regex fails, what the address types mean, and how to validate any IP address in a single Python API call.

1. Why IP address validation matters

IP addresses are fundamental to every networked application. You encounter them in firewall rules, access control lists, rate limiting, geolocation lookups, audit logs, and API allow/deny lists. Accepting an invalid or misclassified IP address can open security holes, break routing logic, or produce misleading analytics.

Security — Firewalls and WAFs rely on accurate IP classification. Treating a public IP as private (or vice versa) can accidentally expose internal services or block legitimate users. If your application accepts user-supplied IP addresses (e.g., for allow-listing), you need to verify that they are syntactically valid and correctly classified.

Access control — Many applications restrict access based on IP ranges. Admin panels might only be accessible from private network addresses, while webhooks must come from known public IPs. Misidentifying an address type breaks these policies.

Geolocation — IP-to-location services only work with public addresses. Passing a private, loopback, or link-local address to a geolocation API returns meaningless results. Detecting the address type before the lookup saves API calls and avoids data quality issues.

Logging and analytics — Storing normalized, validated IP addresses ensures consistent log formats and accurate aggregation. An IPv6 address can be written in many equivalent forms — without normalization, the same address appears as multiple distinct entries in your analytics.


2. IPv4 vs IPv6 — the two standards

The Internet uses two IP address formats that look completely different and have different validation rules.

IPv4 — the familiar dotted-decimal format

IPv4 addresses consist of four octets (0-255) separated by dots, giving a 32-bit address space of roughly 4.3 billion addresses. Examples: 192.168.1.1, 10.0.0.1, 8.8.8.8.

The IPv4 address space was exhausted in 2011 (IANA) and regional registries followed soon after. This is the primary driver behind IPv6 adoption.

IPv6 — the 128-bit successor

IPv6 addresses consist of eight groups of four hexadecimal digits separated by colons, giving a 128-bit address space (3.4 × 1038 addresses). Groups of consecutive zeros can be compressed using :: shorthand.

FormExample
Full expanded2001:0db8:0000:0000:0000:0000:0000:0001
Compressed2001:db8::1
Loopback::1
IPv4-mapped::ffff:192.168.1.1

The transition challenge

Both protocols coexist today. Most networks run dual-stack (IPv4 + IPv6 simultaneously), and transition mechanisms like IPv4-mapped IPv6 addresses (::ffff:192.168.1.1) add further complexity. Your validation logic must handle both formats correctly.


3. IP address types and reserved ranges

Not all IP addresses are created equal. Various ranges are reserved for specific purposes, and knowing the type is critical for security and routing decisions.

IPv4 reserved ranges

TypeRangeRFCPurpose
Private10.0.0.0/8RFC 1918Large internal networks
Private172.16.0.0/12RFC 1918Medium internal networks
Private192.168.0.0/16RFC 1918Home and small office networks
Loopback127.0.0.0/8RFC 1122Localhost communication
Link-local169.254.0.0/16RFC 3927Auto-configuration when no DHCP
Multicast224.0.0.0/4RFC 5771One-to-many communication
Broadcast255.255.255.255/32RFC 919All hosts on local network

IPv6 reserved ranges

Global unicast2000::/3 — routable public addresses
Loopback::1/128 — localhost
Link-localfe80::/10 — auto-configured, single link only
Unique localfc00::/7 — similar to IPv4 private ranges
Multicastff00::/8 — one-to-many delivery

4. Why simple regex fails

The first instinct is to reach for a regular expression. Something like this:

import re

# A common (but broken) IPv4 regex
ipv4_regex = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")

# Looks like it works:
ipv4_regex.match("192.168.1.1")     # match ✓
ipv4_regex.match("8.8.8.8")         # match ✓

# But accepts invalid addresses:
ipv4_regex.match("999.999.999.999") # match ✗  (octets > 255)
ipv4_regex.match("01.02.03.04")     # match ✗  (leading zeros — octal in some parsers)
ipv4_regex.match("192.168.001.001") # match ✗  (leading zeros)

# And tells you nothing about:
# - Is it private or public?
# - Is it a loopback address?
# - What's the normalized form?

IPv6 is even harder — the compressed notation with :: makes regex validation practically impossible to get right:

# IPv6 regex is notoriously complex and fragile
ipv6_regex = re.compile(r"^[0-9a-fA-F:]+$")

# Accepts garbage:
ipv6_regex.match(":::::::")          # match ✗  (not valid)
ipv6_regex.match("gggg::1")         # no match ✓ (but a stricter regex might miss edge cases)

# Cannot handle:
# - IPv4-mapped addresses (::ffff:192.168.1.1)
# - Multiple :: in the same address (invalid but hard to detect)
# - Zone IDs (fe80::1%eth0)
# - Whether the address is global-unicast vs link-local vs multicast

The fundamental issue is that IP address validation requires semantic understanding, not just syntactic pattern matching. You need to verify that octets are in range, that :: appears at most once, that IPv4-mapped suffixes are valid, and then classify the address into its correct type. A regex cannot do all of this reliably.


5. The right solution: one API call

The IsValid IP Address API validates both IPv4 and IPv6 addresses in a single GET request. It handles format validation, version detection, type classification, and normalization — and returns the expanded form for IPv6 addresses.

<20ms
Validation
full parse + type detection
IPv4 + IPv6
Protocols
both standards supported
100/day
Free tier
no credit card

Pass any IP address string — IPv4 or IPv6, compressed or expanded — and the API returns the validated, classified, and normalized result.

Full parameter reference and response schema: IP Address Validation API docs →


6. Python code example

Using the isvalid-sdk Python SDK or the popular requests library. Install with pip install isvalid-sdk or pip install requests.

# ip_validator.py
import os
from isvalid_sdk import IsValidConfig, create_client

iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"]))

# ── Validate an IPv4 address ────────────────────────────────────────────────

v4 = iv.net.ip("192.168.1.1")
print(v4["valid"])       # True
print(v4["version"])     # 4
print(v4["type"])        # 'private'
print(v4["isPrivate"])   # True
print(v4["isLoopback"])  # False
print(v4["normalized"])  # '192.168.1.1'

# ── Validate an IPv6 address ────────────────────────────────────────────────

v6 = iv.net.ip("2001:0db8::1")
print(v6["valid"])       # True
print(v6["version"])     # 6
print(v6["expanded"])    # '2001:0db8:0000:0000:0000:0000:0000:0001'

# ── Check for loopback ──────────────────────────────────────────────────────

lo = iv.net.ip("127.0.0.1")
print(lo["type"])        # 'loopback'
print(lo["isLoopback"])  # True
print(lo["isPrivate"])   # False

In a Flask or Django middleware context, you might use it like this:

# middleware.py (Flask)
from functools import wraps
from flask import request, jsonify, g

def validate_client_ip(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        client_ip = (
            request.headers.get("X-Forwarded-For", "").split(",")[0].strip()
            or request.remote_addr
        )

        try:
            ip_check = validate_ip(client_ip)
        except Exception:
            return jsonify({"error": "IP validation service unavailable"}), 502

        if not ip_check["valid"]:
            return jsonify({"error": "Invalid client IP"}), 400

        # Attach validated IP info to Flask's request context
        g.client_ip = ip_check["normalized"]
        g.ip_version = ip_check["version"]
        g.ip_type = ip_check["type"]
        g.ip_is_private = ip_check["isPrivate"]

        return f(*args, **kwargs)
    return decorated


@app.route("/admin")
@validate_client_ip
def admin_panel():
    # Only allow access from private networks
    if not g.ip_is_private:
        return jsonify({"error": "Access denied — private network only"}), 403

    return jsonify({"message": "Welcome to the admin panel"})
Always use the normalized field when storing IP addresses. This ensures consistent formatting — especially important for IPv6, where the same address can be written in many equivalent ways.

7. cURL example

Validate an IPv4 address:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/net/ip?value=192.168.1.1"

Validate an IPv6 address:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/net/ip?value=2001%3A0db8%3A%3A1"

Validate a public DNS resolver:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/net/ip?value=8.8.8.8"

8. Understanding the response

Private IPv4 address:

{
  "valid": true,
  "version": 4,
  "type": "private",
  "isPrivate": true,
  "isLoopback": false,
  "normalized": "192.168.1.1"
}

IPv6 address with expanded form:

{
  "valid": true,
  "version": 6,
  "type": "global-unicast",
  "isPrivate": false,
  "isLoopback": false,
  "normalized": "2001:db8::1",
  "expanded": "2001:0db8:0000:0000:0000:0000:0000:0001"
}

Invalid IP address:

{
  "valid": false
}
FieldTypeDescription
validbooleanWhether the IP address is syntactically valid
versionnumberIP version: 4 for IPv4, 6 for IPv6
typestringAddress type — IPv4: public, private, loopback, link-local, multicast, broadcast; IPv6: global-unicast, loopback, link-local, unique-local, multicast
isPrivatebooleanWhether the address is in a private/non-routable range
isLoopbackbooleanWhether the address is a loopback address (127.x.x.x or ::1)
normalizedstringThe canonical normalized form of the address
expandedstringFull expanded IPv6 representation with all groups and leading zeros (IPv6 only)
ℹ️The expanded field is only present for IPv6 addresses. It shows the full 8-group, zero-padded form — useful for storage, comparison, and firewall rule generation.

9. Edge cases to handle

IPv4-mapped IPv6 addresses

Addresses like ::ffff:192.168.1.1 embed an IPv4 address inside an IPv6 wrapper. These are common in dual-stack server environments. The API recognizes these and classifies them correctly.

mapped = iv.net.ip("::ffff:192.168.1.1")
print(mapped["valid"])     # True
print(mapped["version"])   # 6
print(mapped["type"])      # 'private' (classified by the embedded IPv4)

Compressed IPv6 notation

The :: shorthand can appear anywhere in an IPv6 address but only once. The API validates this rule and returns both the compressed (normalized) and fully expanded forms.

compressed = iv.net.ip("fe80::1")
print(compressed["normalized"])  # 'fe80::1'
print(compressed["expanded"])    # 'fe80:0000:0000:0000:0000:0000:0000:0001'

# Invalid: double :: is rejected
bad = iv.net.ip("fe80::1::2")
print(bad["valid"])  # False

CIDR notation

CIDR notation like 192.168.1.0/24 represents a network range, not a single host address. The API validates individual IP addresses, not CIDR ranges. Strip the prefix length before validating if you need to check the network address itself.

Zone IDs

IPv6 link-local addresses sometimes include a zone ID (e.g., fe80::1%eth0). Zone IDs are interface-specific and not part of the address itself. Strip the zone ID (everything after %) before passing the address to the API.

Leading zeros in IPv4

Some parsers treat leading zeros as octal notation: 010.010.010.010 could mean 8.8.8.8 (octal) or 10.10.10.10 (decimal). The API handles this ambiguity and returns the correct normalized form.


10. Summary

Do not validate IP addresses with regex — it misses octet ranges, IPv6 shorthand, and mapped addresses
Do not assume all IPs are IPv4 — IPv6 adoption is accelerating and dual-stack is standard
Do not pass private or loopback IPs to geolocation services — they return meaningless results
Do not store IPv6 in user-entered format — the same address has many equivalent representations
Validate format, version, and type in a single API call
Store the normalized form for consistent log aggregation and firewall rules
Use the expanded field for IPv6 comparison and storage
Check isPrivate and isLoopback for access control decisions

See also

Validate IP addresses instantly

Free tier includes 100 API calls per day. No credit card required. Supports IPv4 and IPv6 with full type detection.