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.
- Warm-up. Given
172.16.40.200/21, print the network address, the broadcast address, and the number of usable hosts. - 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. - Overlap checker. Write a function
conflicts(new, existing_list)that returns the list of existing networks a proposed network overlaps (empty list if none). - Subnet carver. Split
10.10.0.0/24into/28subnets and print each subnet number alongside its usable host range (first–last). - 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.