You already think in subnets every day. You squint at 10.20.30.64/26 and your brain coughs up the network address, the broadcast, and the usable range. 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.
Welcome to Week 2 of the series. Week 1 taught you the language; this week we point it at real network problems using the standard library and the core automation tools. Today: the module you will reach for more than almost any other.
Addresses, Networks, and Interfaces
The module gives you three core object types. Knowing which one you want 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 you parse show ip interface brief-style data, ip_interface is usually what you want.
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. If you are feeding it config lines where someone wrote the interface IP with a prefix, pass strict=False to have it mask down to the network for you:
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)}")
Want 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 you allocate)
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
A teammate asks for 10.50.2.0/25 for a new VLAN. Before you reserve it, you want to know: is it inside your 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")
Run it and you get 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. Try 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
We pass strict=False 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. We met defaultdict 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.