The ipaddress Module: Subnets, Supernets, and Host Iteration in Python

Network engineers think in subnets every day. A glance at 10.20.30.64/26 and the network address, the broadcast, and the usable range all come to mind. The problem is doing that at scale — across a spreadsheet of 300 subnets, or while validating that a new allocation does not overlap an existing one. This is exactly where Python’s built-in ipaddress module earns its keep. No pip install, no third-party anything: it ships with Python and it understands IPv4, IPv6, hosts, networks, and the relationships between them.

This is the start of Week 2. Week 1 covered the language; this week points it at real network problems using the standard library and the core automation tools. Today: the module reached for more than almost any other.

Addresses, Networks, and Interfaces

The module provides three core object types. Knowing which one is needed is half the battle.

import ipaddress

# A bare host address — no mask attached
addr = ipaddress.ip_address("10.20.30.65")
print(addr.version)          # 4
print(addr.is_private)       # True

# A network — an address block with a prefix
net = ipaddress.ip_network("10.20.30.64/26")
print(net.network_address)   # 10.20.30.64
print(net.broadcast_address) # 10.20.30.127
print(net.netmask)           # 255.255.255.192
print(net.num_addresses)     # 64

# An interface — a host address that KNOWS its network
iface = ipaddress.ip_interface("10.20.30.65/26")
print(iface.ip)              # 10.20.30.65
print(iface.network)         # 10.20.30.64/26

Notice the split. ip_address is just the host. ip_network is the block (and by default it refuses a host bit set — more on that in a second). ip_interface is what an actual router interface has: an address plus the network it lives on. When parsing show ip interface brief-style data, ip_interface is usually the right choice.

The “Host Bits Set” Gotcha

This trips up everyone once:

>>> ipaddress.ip_network("10.20.30.65/26")
ValueError: 10.20.30.65/26 has host bits set

ip_network expects the network address, not a host inside it. For config lines where someone wrote the interface IP with a prefix, passing strict=False makes it mask down to the network automatically:

net = ipaddress.ip_network("10.20.30.65/26", strict=False)
print(net)   # 10.20.30.64/26

Iterating Hosts — Build a Real Allocation

The killer feature: a network is iterable. .hosts() yields every usable host (it skips the network and broadcast addresses for IPv4).

net = ipaddress.ip_network("192.168.10.0/29")

for host in net.hosts():
    print(host)
# 192.168.10.1
# 192.168.10.2
# 192.168.10.3
# 192.168.10.4
# 192.168.10.5
# 192.168.10.6

# Grab the first usable as the gateway, hand out the rest
hosts = list(net.hosts())
gateway = hosts[0]
pool = hosts[1:]
print(f"Gateway: {gateway}, DHCP pool size: {len(pool)}")

To get every address including network and broadcast, iterate the network object directly instead of calling .hosts().

Membership, Overlap, and Subnetting

These three operations are why the module exists. Memorize them.

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

# 1. Is this address inside this block?
print(ipaddress.ip_address("10.0.5.20") in net)   # True
print(ipaddress.ip_address("10.1.5.20") in net)    # False

# 2. Do two blocks overlap? (clash detection before allocating)
a = ipaddress.ip_network("10.0.0.0/24")
b = ipaddress.ip_network("10.0.0.128/25")
print(a.overlaps(b))   # True

# 3. Carve a big block into smaller ones
parent = ipaddress.ip_network("10.50.0.0/22")
for child in parent.subnets(new_prefix=24):
    print(child)
# 10.50.0.0/24
# 10.50.1.0/24
# 10.50.2.0/24
# 10.50.3.0/24

And going the other way — collapse a list of contiguous subnets into the largest possible supernets:

nets = [ipaddress.ip_network(n) for n in
        ["10.50.0.0/24", "10.50.1.0/24", "10.50.2.0/24", "10.50.3.0/24"]]
for sn in ipaddress.collapse_addresses(nets):
    print(sn)   # 10.50.0.0/22

Cisco Context: Validating an Allocation Request

Say a request comes in for 10.50.2.0/25 for a new VLAN. Before reserving it, two questions matter: is it inside the assigned supernet, and does it collide with anything already allocated? That is six lines of Python.

import ipaddress

assigned   = ipaddress.ip_network("10.50.0.0/22")
existing   = [ipaddress.ip_network(n) for n in
              ["10.50.0.0/24", "10.50.1.0/24", "10.50.2.0/26"]]
requested  = ipaddress.ip_network("10.50.2.0/25")

if not requested.subnet_of(assigned):
    print(f"REJECT: {requested} is outside our space {assigned}")
elif any(requested.overlaps(e) for e in existing):
    clash = [str(e) for e in existing if requested.overlaps(e)]
    print(f"REJECT: {requested} overlaps {clash}")
else:
    print(f"OK: {requested} is free to allocate")

Running it produces REJECT: 10.50.2.0/25 overlaps ['10.50.2.0/26']. That is a code review for IP space that never sleeps.

Exercises

Use only the ipaddress module. Attempt each before opening the answer.

  1. Warm-up. Given 172.16.40.200/21, print the network address, the broadcast address, and the number of usable hosts.
  2. Pool builder. For 192.168.100.0/27, print the first usable address as the gateway and the last usable address as the DHCP server, then report how many addresses are left for clients.
  3. Overlap checker. Write a function conflicts(new, existing_list) that returns the list of existing networks a proposed network overlaps (empty list if none).
  4. Subnet carver. Split 10.10.0.0/24 into /28 subnets and print each subnet number alongside its usable host range (first–last).
  5. Challenge. Given a messy list of interface strings like "10.1.1.5/24", "10.1.1.9/24", "10.2.2.3/30", group the addresses by the network they belong to and print each network with the count of hosts seen on it.

Answers

Show answers

1. Warm-up

import ipaddress
net = ipaddress.ip_network("172.16.40.200/21", strict=False)
print(net.network_address)    # 172.16.40.0
print(net.broadcast_address)  # 172.16.47.255
print(net.num_addresses - 2)  # 2046

strict=False is needed because .200 is a host bit; the module masks it down to 172.16.40.0/21.

2. Pool builder

net = ipaddress.ip_network("192.168.100.0/27")
hosts = list(net.hosts())
gateway, dhcp = hosts[0], hosts[-1]
clients = hosts[1:-1]
print(f"Gateway {gateway}, DHCP server {dhcp}, {len(clients)} client addresses")
# Gateway 192.168.100.1, DHCP server 192.168.100.30, 28 client addresses

3. Overlap checker

def conflicts(new, existing_list):
    new = ipaddress.ip_network(new)
    return [str(e) for e in existing_list
            if new.overlaps(ipaddress.ip_network(e))]

print(conflicts("10.0.0.0/24", ["10.0.0.128/25", "10.1.0.0/24"]))
# ['10.0.0.128/25']

4. Subnet carver

parent = ipaddress.ip_network("10.10.0.0/24")
for sub in parent.subnets(new_prefix=28):
    hosts = list(sub.hosts())
    print(f"{sub}: {hosts[0]} - {hosts[-1]}")
# 10.10.0.0/28: 10.10.0.1 - 10.10.0.14
# ... and so on for all 16 /28s

5. Challenge

from collections import defaultdict
import ipaddress

ifaces = ["10.1.1.5/24", "10.1.1.9/24", "10.2.2.3/30"]
groups = defaultdict(list)
for s in ifaces:
    i = ipaddress.ip_interface(s)
    groups[i.network].append(i.ip)

for net, ips in groups.items():
    print(f"{net}: {len(ips)} hosts -> {[str(ip) for ip in ips]}")
# 10.1.1.0/24: 2 hosts -> ['10.1.1.5', '10.1.1.9']
# 10.2.2.0/30: 1 hosts -> ['10.2.2.3']

The trick is ip_interface: it carries both the host and the network, so i.network becomes a natural grouping key. defaultdict appeared back in Week 1 — this is it paying off.


Previously: Files, Exceptions, and Logging. Coming tomorrow — Regex with re: extracting interface counters and parsing show output without writing a parser by hand.

This is Day 8 of the 21‑post Python for Network Engineers series.