Check CIDR Overlaps with Python Before Deployment
On this page
An inventory lists both 192.0.2.0/24 and 192.0.2.128/25. Each entry is valid, but their address ranges overlap. Discovering this after deploying containers, a VPN, or a test environment means revisiting the configuration under pressure.
This beginner-to-intermediate tutorial builds a read-only CIDR overlap checker with Python’s standard library. It reads a local inventory, reports the first conflicting pair with line numbers, and uses exit codes to stop later steps. It does not scan networks or change Linux routes, firewall rules, or service configuration.
1. Method: compare ranges without enumerating addresses
CIDR uses a network address followed by a prefix length: /24 means the first 24 bits of an IPv4 address identify the network. For the distinction between individual addresses and networks, start with IPv4/IPv6 address validation in Python. Here we check intersections between ranges, not the exercise-specific string rules.
The inventory follows these rules:
- One IPv4 or IPv6 network per line, with an explicit numeric prefix. Blank lines and comments after
#are allowed. - Network addresses must align with their prefixes. Reject host bits such as
192.0.2.9/24instead of silently changing the range. - Identical networks and containment count as overlaps. Adjacent ranges without an intersection pass. Compare IPv4 and IPv6 separately.
- Validate the entire file before finding the first conflicting pair in input order. An empty inventory is an input error, not a pass.
The official ip_network() documentation explains strict parsing; overlaps() checks network intersections. This example never expands a host list, even for large IPv6 ranges.
Overlap is not automatically a business error. Containment may be intentional in route summaries or allowlists. Use this tool for inventories whose networks must be disjoint within one planning scope, not as a general route-table validator.
2. Setup: create an isolated demo directory
Use Python 3.9 or newer, with no third-party dependencies. Commands were tested on Linux with Python 3.11.4 and work in bash or zsh.
python3 --version
mkdir cidr-check-demo
cd cidr-check-demo
Save the next section as check_cidrs.py in the current directory, or download the identical complete script. The later redirects create demo fixtures and overwrite matching names; use a fresh directory when repeating the experiment.
3. Implementation: parse strictly and compare matching versions
"""Find the first overlap in a small local CIDR inventory. Python 3.9+."""
import argparse
import ipaddress
from pathlib import Path
import sys
MAX_BYTES = 128 * 1024
MAX_NETWORKS = 1000
def read_networks(path):
if not path.is_file():
raise ValueError("input must be a regular file")
with path.open("rb") as stream:
raw = stream.read(MAX_BYTES + 1)
if len(raw) > MAX_BYTES:
raise ValueError(f"input exceeds {MAX_BYTES} bytes")
text = raw.decode("utf-8")
if text.startswith("\ufeff"):
raise ValueError("remove the UTF-8 BOM first")
rows = []
for line_number, line in enumerate(text.splitlines(), 1):
token = line.split("#", 1)[0].strip()
if not token:
continue
if len(rows) >= MAX_NETWORKS:
raise ValueError(f"more than {MAX_NETWORKS} networks")
if token.count("/") != 1 or "%" in token:
raise ValueError(f"line {line_number}: expected unscoped IP/prefix")
prefix = token.split("/", 1)[1]
if not prefix.isascii() or not prefix.isdecimal():
raise ValueError(f"line {line_number}: prefix must be decimal digits")
try:
network = ipaddress.ip_network(token, strict=True)
except ValueError as exc:
raise ValueError(f"line {line_number}: {exc}") from exc
rows.append((line_number, network))
if not rows:
raise ValueError("no networks found")
return rows
def first_overlap(rows):
for index, (left_line, left) in enumerate(rows):
for right_line, right in rows[index + 1:]:
if left.version == right.version and left.overlaps(right):
return left_line, left, right_line, right
return None
def main():
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
parser.add_argument("file", type=Path, help="UTF-8 file: one CIDR per line")
args = parser.parse_args()
try:
rows = read_networks(args.file)
conflict = first_overlap(rows)
except (OSError, ValueError) as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
if conflict:
left_line, left, right_line, right = conflict
print(f"OVERLAP: line {left_line} {left} <-> line {right_line} {right}")
return 1
print(f"OK: {len(rows)} networks; no overlaps within each IP version")
return 0
if __name__ == "__main__":
raise SystemExit(main())
The script only reads small regular files in a trusted local directory, with no active writer: at most 128 KiB and 1000 networks. It follows symlinks. It is not designed for hostile paths, concurrent file replacement, or network filesystems that may hang; a byte limit is not timeout protection.
Requiring /, numeric prefixes, and no % scope identifier are additional input rules defined by this tool. A format accepted by the library need not be appropriate for an inventory.
Python’s official introduction shows that strict=False clears host bits, while ip_interface() represents a host address together with its network. This tool keeps strict=True so the maintainer must confirm intent before correcting the inventory. Do not silently broaden a range to make an error disappear.
4. Usage: distinguish overlaps from failed checks
- View help and create an overlapping inventory:
python3 check_cidrs.py --help
cat > networks.txt <<'CIDRS'
# planned subnets
192.0.2.0/24
192.0.2.128/25
2001:db8:1::/64
CIDRS
if python3 check_cidrs.py networks.txt; then
rc=0
else
rc=$?
fi
printf 'exit=%s\n' "$rc"
After the help text, the check prints:
OVERLAP: line 2 192.0.2.0/24 <-> line 3 192.0.2.128/25
exit=1
Line numbers include comments and blank lines. The if branch preserves nonzero exit codes, so the result remains visible even when the shell has set -e enabled.
- If the intended plan is two disjoint
/25networks, create a separate corrected inventory:
cat > networks-fixed.txt <<'CIDRS'
192.0.2.0/25
192.0.2.128/25
2001:db8:1::/64
CIDRS
python3 check_cidrs.py networks-fixed.txt
OK: 3 networks; no overlaps within each IP version
This is a sample plan, not advice to change a live network. Confirm whether the /24 intentionally contains the later subnet before deciding what to edit.
| Exit code | Meaning | Output destination |
|---|---|---|
| 0 | Entire inventory is valid; no overlaps found | stdout, starting with OK: |
| 1 | First overlapping pair found | stdout, starting with OVERLAP: |
| 2 | File, encoding, format, size, or argument error | stderr; no success report |
--help prints help and exits 0. Propagate failure explicitly in automation. Run the next snippet inside a standalone script: failure will terminate that script.
if python3 check_cidrs.py networks-fixed.txt; then
printf 'NEXT: inventory passed; review the deployment separately\n'
else
rc=$?
printf 'STOP: CIDR preflight exit=%s\n' "$rc" >&2
exit "$rc"
fi
This only permits a later, separately reviewed step; it performs no deployment. A passing inventory does not establish connectivity. Continue with system DNS resolution troubleshooting for hostname failures and TCP port checks for service reachability.
5. Validation: boundaries, IPv6, and unchanged inputs
Run this self-test in the same directory. It creates only its own temporary directory, checks actual exit codes and output categories, and verifies that each input’s SHA-256 remains unchanged:
python3 - <<'PY_CHECK'
import hashlib
from pathlib import Path
import subprocess
import sys
import tempfile
cases = [
("adjacent", "192.0.2.0/25\n192.0.2.128/25\n", 0),
("nested", "192.0.2.0/24\n192.0.2.128/25\n", 1),
("duplicate", "192.0.2.0/24\n192.0.2.0/24\n", 1),
("ipv6", "2001:db8::/32\n2001:db8:1::/48\n", 1),
("mixed", "192.0.2.0/24\n2001:db8::/32\n", 0),
("host-bits", "192.0.2.9/24\n", 2),
("empty", "# nothing planned yet\n", 2),
]
with tempfile.TemporaryDirectory(prefix="cidr-tests-", dir=".") as folder:
path = Path(folder) / "input.txt"
for label, text, expected in cases:
path.write_text(text, encoding="utf-8")
before = hashlib.sha256(path.read_bytes()).digest()
result = subprocess.run(
[sys.executable, "check_cidrs.py", str(path)],
capture_output=True, text=True, timeout=10,
)
assert result.returncode == expected, (label, result)
if expected == 2:
assert not result.stdout and result.stderr.startswith("ERROR:")
else:
assert not result.stderr
assert result.stdout.startswith("OVERLAP:" if expected else "OK:")
assert hashlib.sha256(path.read_bytes()).digest() == before
print(f"PASS: {label}; exit={result.returncode}; input unchanged")
PY_CHECK
Expect seven PASS: lines. Adjacent networks and mixed versions pass; containment, duplicates, and IPv6 containment return 1; host bits and an empty inventory return 2. Any failed assertion stops the test instead of reporting success.
You can also reproduce a host-bit error. The diagnostic goes to stderr:
printf '192.0.2.9/24\n' > host-bits.txt
if python3 check_cidrs.py host-bits.txt; then
rc=0
else
rc=$?
fi
printf 'exit=%s\n' "$rc"
ERROR: line 1: 192.0.2.9/24 has host bits set
exit=2
6. Troubleshooting: never mistake invalid input for a safe inventory
| Symptom | Direct action |
|---|---|
has host bits set |
Confirm whether the entry describes a network or an interface. Supply an aligned network address; do not blindly switch to strict=False. |
expected unscoped IP/prefix |
Include an explicit numeric prefix such as /24 or /64. Hostnames and %eth0 are not supported. |
prefix must be decimal digits |
Use /24, not /255.255.255.0 or trailing prose; put comments after #. |
| Encoding error or BOM message | Save a separate UTF-8 copy without a BOM, preserve the original, and check again. |
| Only one conflict appears | This finds the first pair. Review and fix it, then rerun; it is not an all-conflicts report. |
| File or network-count limit exceeded | Do not validate independent chunks and claim the full set is disjoint: overlaps can cross chunk boundaries. Use an algorithm that covers the entire inventory. |
For 1000 networks, pairwise comparison requires at most 499500 checks, with O(n²) complexity. The limit suits small configuration reviews, not a large address-management platform. The tool does not determine public routability or inspect actual routes, firewall rules, or address usage.
7. Summary
Parse the inventory strictly, then compare ranges within the same IP version to catch containment and duplicate allocations before deployment. Keep line numbers and distinct exit codes, and test failure cases before connecting the checker to automation. A pass only means this inventory meets the disjoint-range rule; validate the actual network separately.
- Author: yanzhi
- Original article: https://www.bobobk.com/en/python-cidr-overlap-check.html
- License: This work is licensed under CC BY-NC-ND 4.0. See the license for attribution, noncommercial use and no-derivatives terms.
Related articles
- Compare Configuration Files with Python: Unified Diffs, Line Endings, and Exit Codes
- Wait for a File in Python: Timeouts, Atomic Handoffs, and Exit Codes
- Python File Permissions on Linux: umask, Mode 600, and No Overwrites
- Clean Up Old Logs with Python: Preview First, Skip Links and Subdirectories
- Merge Sorted Logs by Timestamp in Python with heapq.merge