Build the mental model
A subnet calculator is the first tool most network engineers write, and it is a good project precisely because it forces every piece of addressing theory to become executable. Four earlier ideas meet here. First, the structure of an IPv4 address as thirty-two bits rather than four decimal numbers -- the ipaddress module hides the arithmetic, but the whole program only makes sense if you can picture the bits underneath it. Second, the subnet mask as the boundary between the network portion and the host portion: base.subnets(new_prefix=...) is nothing more than sliding that boundary to the right. Third, the borrowing rule -- to carve a block into at least N pieces you take the smallest n where 2^n is greater than or equal to N, which is what bits_to_borrow computes. Ask for four subnets and you borrow two bits; ask for five and you still borrow three, because subnetting only ever splits in powers of two and the leftover subnets are simply unused capacity. Fourth, the 2^n - 2 usable-host rule and, crucially, where it stops being true.
That last point is why the program has a dedicated usable_range function instead of a one-line subtraction. In a normal subnet the all-zeros address names the network and the all-ones address is the broadcast, so both are unusable. A /31 has only two addresses, and RFC 3021 redefines them: on a point-to-point link there is no broadcast, so both ends are usable. A /32 is a single host route -- one address, with no network and broadcast pair at all. A naive calculator reports zero and negative one usable hosts for those two cases. Handling them is what separates a toy from something you would actually trust with a real address plan.
SUBNET CALCULATOR DATA FLOW
---------------------------
INPUT PROCESSING OUTPUT
----- ---------- ------
+---------------+
| 192.168.10.0 |
| /24 |---+
+---------------+ |
v
+---------------+ +------------------------------+
| need 4 subnets|->| bits_to_borrow: 2^n >= 4 |
+---------------+ | n = 2 bits borrowed|
+------------------------------+
|
v
+------------------------------+
| new prefix = /24 + 2 = /26 |
| base.subnets(new_prefix=26) |
+------------------------------+
|
v
+------------------------------+ +------------+
| usable_range() per subnet | | aligned |
| /32 -> 1 (host route) |---->| allocation |
| /31 -> 2 (RFC 3021) | | table |
| else -> 2^n - 2 | +------------+
+------------------------------+Connect it to a real scenario
Address planning is one of the few network tasks where a mistake is expensive and silent. You are handed a 10.20.0.0/16 for a new environment and told to carve out subnets for three availability zones, each with a public, private and database tier. Done by hand in a spreadsheet, the failure mode is not a wrong broadcast address -- it is two blocks that quietly overlap, or a boundary off a power-of-two edge that lets a summary route swallow a neighbour's range. Nothing errors. Traffic just goes to the wrong place months later, usually the first time you peer that VPC with another.
A tool like this makes the plan reviewable. Because every boundary comes from base.subnets() rather than arithmetic you typed, overlaps are impossible by construction, and the printed table is something a colleague can check in ten seconds. The second thing it buys is the sizing conversation. Ask for five subnets and it borrows three bits and shows you eight, so you see immediately that you are spending capacity you may want later. Split a /24 four ways and each tier gets sixty-two hosts; if a tier will ever hold more, you find out now rather than during a scaling event. And the /31 row matters in practice: point-to-point router links are routinely /31 or /30, and a calculator that reports zero usable hosts for a /31 will send someone hunting for a bug that does not exist.
Try the working example
import ipaddress
def bits_to_borrow(required):
"""Smallest n such that 2**n >= required subnets."""
n = 0
while (1 << n) < required:
n += 1
return n
def usable_range(net):
"""First host, last host, usable count -- honouring /31 and /32."""
if net.prefixlen == net.max_prefixlen:
# /32: one address, a host route. No network/broadcast split at all.
return net.network_address, net.network_address, 1
if net.prefixlen == net.max_prefixlen - 1:
# /31: RFC 3021 point-to-point link. Both addresses are usable.
return net.network_address, net.broadcast_address, 2
# Normal case: drop the network address and the broadcast address.
return net.network_address + 1, net.broadcast_address - 1, net.num_addresses - 2
def allocate(cidr, required):
base = ipaddress.ip_network(cidr)
borrow = bits_to_borrow(required)
new_prefix = base.prefixlen + borrow
if new_prefix > base.max_prefixlen:
raise ValueError("cannot fit %d subnets inside %s" % (required, base))
return base, borrow, new_prefix, list(base.subnets(new_prefix=new_prefix))
def print_plan(cidr, required):
base, borrow, new_prefix, subnets = allocate(cidr, required)
print("SUBNET ALLOCATION PLAN")
print("=" * 72)
print("Base block : %s" % base)
print("Base netmask : %s" % base.netmask)
print("Subnets needed : %d" % required)
print("Bits borrowed : %d (2^%d = %d subnets)" % (borrow, borrow, 1 << borrow))
print("New prefix : /%d (netmask %s)" % (new_prefix, subnets[0].netmask))
print("-" * 72)
print("%-3s %-18s %-15s %-15s %s" % ("#", "NETWORK", "FIRST HOST", "LAST HOST", "BROADCAST"))
print("-" * 72)
for i, net in enumerate(subnets, start=1):
first, last, _ = usable_range(net)
bcast = "n/a" if net.prefixlen >= net.max_prefixlen - 1 else str(net.broadcast_address)
print("%-3d %-18s %-15s %-15s %s" % (i, net.with_prefixlen, first, last, bcast))
print("-" * 72)
print("Usable hosts per subnet: %d" % usable_range(subnets[0])[2])
print()
def print_edge_cases():
print("EDGE CASES: WHERE 2^n - 2 STOPS BEING TRUE")
print("=" * 72)
print("%-16s %-8s %-15s %-15s %s" % ("BLOCK", "ADDRS", "FIRST HOST", "LAST HOST", "USABLE"))
print("-" * 72)
for cidr in ["10.0.0.0/29", "10.0.0.0/30", "10.0.0.8/31", "10.0.0.12/32"]:
net = ipaddress.ip_network(cidr)
first, last, count = usable_range(net)
print("%-16s %-8d %-15s %-15s %d" % (net.with_prefixlen, net.num_addresses, first, last, count))
print("-" * 72)
print("A /31 has no broadcast address (RFC 3021), so both addresses are")
print("usable. A /32 is a single host route. Blindly applying 2^n - 2")
print("would report 0 and -1 usable hosts for these two cases.")
print_plan("192.168.10.0/24", 4)
print_edge_cases()
SUBNET ALLOCATION PLAN
========================================================================
Base block : 192.168.10.0/24
Base netmask : 255.255.255.0
Subnets needed : 4
Bits borrowed : 2 (2^2 = 4 subnets)
New prefix : /26 (netmask 255.255.255.192)
------------------------------------------------------------------------
# NETWORK FIRST HOST LAST HOST BROADCAST
------------------------------------------------------------------------
1 192.168.10.0/26 192.168.10.1 192.168.10.62 192.168.10.63
2 192.168.10.64/26 192.168.10.65 192.168.10.126 192.168.10.127
3 192.168.10.128/26 192.168.10.129 192.168.10.190 192.168.10.191
4 192.168.10.192/26 192.168.10.193 192.168.10.254 192.168.10.255
------------------------------------------------------------------------
Usable hosts per subnet: 62
EDGE CASES: WHERE 2^n - 2 STOPS BEING TRUE
========================================================================
BLOCK ADDRS FIRST HOST LAST HOST USABLE
------------------------------------------------------------------------
10.0.0.0/29 8 10.0.0.1 10.0.0.6 6
10.0.0.0/30 4 10.0.0.1 10.0.0.2 2
10.0.0.8/31 2 10.0.0.8 10.0.0.9 2
10.0.0.12/32 1 10.0.0.12 10.0.0.12 1
------------------------------------------------------------------------
A /31 has no broadcast address (RFC 3021), so both addresses are
usable. A /32 is a single host route. Blindly applying 2^n - 2
would report 0 and -1 usable hosts for these two cases.
5-minute try-it
Extend the program so it can also size subnets from a required number of hosts instead of a required number of subnets. Write hosts_to_prefix(base, hosts_needed) that finds the smallest number of host bits h where 2^h - 2 is at least hosts_needed, then computes new_prefix = 32 - h. Run it on 192.168.10.0/24 asking for 30 hosts per subnet: you should get /27, eight subnets, thirty usable hosts each. Then ask for 300 hosts and make sure it raises a clear error rather than silently producing a prefix shorter than the base block.
One important caution
Applying 2^n - 2 unconditionally. For a /31 it reports zero usable hosts and for a /32 it reports negative one, which sends people hunting for a bug in an address plan that is actually correct. Point-to-point links are routinely /31 or /30, so this case turns up in real work almost immediately -- give the calculator an explicit branch for prefixes at and just below max_prefixlen.
Confusing the number of subnets with the number of bits to borrow. Asking for five subnets does not borrow five bits, it borrows three, because subnetting only splits in powers of two. Writing new_prefix = base.prefixlen + required instead of + bits_to_borrow(required) produces a plan that looks plausible and is wildly wrong: a /24 split 'five ways' becomes thirty-two subnets of /29.
RFC 3021 -- Using 31-Bit Prefixes on IPv4 Point-to-Point Links — Computer Networking