Python ipaddress: Validate and Subnet IP Addresses

Python's standard ipaddress module provides a robust, built-in framework for inspecting, manipulating, and validating IPv4 and IPv6 network addresses without external dependencies. This article covers how to use the module to check whether an IP string is valid, inspect network boundaries, perform subnet calculations, and determine whether specific hosts belong to a given subnet.

Validating IP Addresses

Validating IP addresses manually using regular expressions can be error-prone and complex, especially with IPv6 notation. The ipaddress.ip_address() factory function validates both IPv4 and IPv6 addresses natively by attempting to instantiate an address object, raising a ValueError if the format is invalid.

import ipaddress

def validate_ip(ip_str):
    try:
        ip_obj = ipaddress.ip_address(ip_str)
        return True, ip_obj.version
    except ValueError:
        return False, None

# Examples
print(validate_ip("192.168.1.1"))       # Output: (True, 4)
print(validate_ip("2001:db8::1"))       # Output: (True, 6)
print(validate_ip("999.999.999.999"))   # Output: (False, None)

The returned object exposes useful properties such as is_private, is_global, is_multicast, and is_loopback, allowing programmatic inspection of the address scope.

Defining and Validating Networks

To handle CIDR blocks, use the ipaddress.ip_network() function. By default, it operates in strict mode (strict=True), which ensures that host bits are not set in the network definition.

# Valid network definition
net = ipaddress.ip_network("192.168.1.0/24")

# Strict mode enforcement
try:
    # Fails because host bit (.5) is set for a /24 network
    invalid_net = ipaddress.ip_network("192.168.1.5/24")
except ValueError as e:
    print(e)  # 192.168.1.5/24 has host bits set

# Supplying strict=False masks host bits automatically
normalized_net = ipaddress.ip_network("192.168.1.5/24", strict=False)
print(normalized_net)  # Output: 192.168.1.0/24

Extracting Network Details

An IPv4Network or IPv6Network object exposes attributes for key routing details:

net = ipaddress.ip_network("10.0.0.0/22")

print(f"Netmask: {net.netmask}")                     # 255.255.252.0
print(f"Hostmask: {net.hostmask}")                   # 0.0.3.255
print(f"Broadcast: {net.broadcast_address}")         # 10.0.3.255
print(f"Total Addresses: {net.num_addresses}")       # 1024

The .hosts() method yields a generator of usable host addresses within the network, omitting the network and broadcast addresses.

hosts = list(net.hosts())
print(f"First usable host: {hosts[0]}")  # 10.0.0.1
print(f"Last usable host: {hosts[-1]}")  # 10.0.3.254

Subnetting and Supernetting

The module includes built-in methods to divide networks into smaller subnets or merge them into larger ones.

Dividing Networks with subnets()

Use the .subnets() method to split a network into smaller allocations. You can define the prefix length increase using prefixlen_diff or define the target prefix length using new_prefix.

parent_net = ipaddress.ip_network("172.16.0.0/16")

# Split a /16 into /18 subnets (prefixlen_diff=2)
subnets = list(parent_net.subnets(prefixlen_diff=2))
for subnet in subnets:
    print(subnet)
# Output:
# 172.16.0.0/18
# 172.16.64.0/18
# 172.16.128.0/18
# 172.16.192.0/18

Combining Networks with supernet()

The .supernet() method calculates the parent network containing the current subnet:

net = ipaddress.ip_network("192.168.1.0/24")
super_net = net.supernet(prefixlen_diff=1)
print(super_net)  # Output: 192.168.0.0/23

Checking Subnet Membership

The ipaddress module supports the Python in operator to verify if an IP address falls within a network block or if one subnet is entirely contained within another.

network = ipaddress.ip_network("192.168.0.0/16")
ip_a = ipaddress.ip_address("192.168.10.25")
ip_b = ipaddress.ip_address("10.0.0.1")

print(ip_a in network)  # True
print(ip_b in network)  # False

# Subnet containment
small_subnet = ipaddress.ip_network("192.168.5.0/24")
print(small_subnet.subnet_of(network))  # True