This Python script collapses a list of CIDR blocks into the smallest equivalent set — merging adjacent and removing overlapping prefixes with the standard library's ipaddress.collapse_addresses — and reports how many addresses the list covers. It makes no network request: aggregating a CIDR list you already have is pure local set arithmetic.
placeholder
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""cidr-aggregate.py -- collapse a list of CIDR blocks into the smallest
equivalent set, and count the addresses they cover.
This recipe does NOT call the ip.xakki.pro API -- collapsing/aggregating a
CIDR list you already have is pure local set arithmetic
(ipaddress.collapse_addresses from the standard library), the same honest
"no API needed here" case as subnet-membership.js in this section.
Reads a text file with one CIDR block per line (IPv4 and IPv6 may be mixed
freely -- each family is collapsed separately, since a v4 prefix and a v6
prefix can never overlap or merge). Prints the minimal equivalent CIDR set
per family, plus how many individual addresses the input covers before and
after collapsing (collapsing removes redundant overlap, it never changes
the covered address count).
Zero dependencies -- stdlib only (ipaddress, argparse). Python 3.8+.
Usage:
python3 cidr-aggregate.py prefixes.txt
"""
import argparse
import ipaddress
import sys
def main():
parser = argparse.ArgumentParser(description="Collapse a list of CIDR blocks into the minimal equivalent set.")
parser.add_argument("input_file", help="Path to a text file, one CIDR block per line.")
args = parser.parse_args()
v4_networks, v6_networks = [], []
errors = []
with open(args.input_file, encoding="utf-8") as f:
for line_no, raw_line in enumerate(f, start=1):
line = raw_line.strip()
if not line or line.startswith("#"):
continue
try:
# strict=False: "10.1.2.3/24" is accepted and normalized to
# "10.1.2.0/24" rather than rejected for having host bits set
# -- this recipe aggregates real-world lists, which routinely
# contain that shape.
network = ipaddress.ip_network(line, strict=False)
except ValueError as e:
errors.append("line {}: {!r} -- {}".format(line_no, line, e))
continue
(v6_networks if network.version == 6 else v4_networks).append(network)
if errors:
print("skipped {} malformed line(s):".format(len(errors)), file=sys.stderr)
for err in errors:
print(" " + err, file=sys.stderr)
for family, networks in (("IPv4", v4_networks), ("IPv6", v6_networks)):
if not networks:
continue
collapsed = list(ipaddress.collapse_addresses(networks))
addresses_before = sum(n.num_addresses for n in networks)
addresses_after = sum(n.num_addresses for n in collapsed)
print("\n{}: {} input block(s) -> {} collapsed block(s)".format(family, len(networks), len(collapsed)))
print(" addresses covered: {} (collapsing never changes this total, "
"it only removes redundant overlap)".format(addresses_after))
if addresses_before != addresses_after:
# Only possible if the input itself double-counted overlapping
# ranges before collapsing folded them together.
print(" (raw sum of individual input blocks was {}, {} of which was overlap)".format(
addresses_before, addresses_before - addresses_after))
for network in collapsed:
print(" " + str(network))
if not v4_networks and not v6_networks:
print("no valid CIDR blocks found in {}".format(args.input_file), file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
This script is MIT-licensed (SPDX-License-Identifier: MIT header at the top of the file) — copy, modify and reuse it freely in your own project, no attribution required.
IPv4 and IPv6 entries are collapsed SEPARATELY and can never merge into each other — a v4 prefix and a v6 prefix occupy entirely different address spaces, so this script buckets the input by family before calling collapse_addresses once per family. "10.1.2.3/24" is accepted (strict=False) and normalized down to "10.1.2.0/24" rather than rejected for having host bits set, since real-world exported prefix lists routinely contain that shape.
Collapsing never changes the total number of addresses covered — it only removes redundant overlap (a /24 fully contained in a /16 in the same list contributes nothing new) and merges exactly-adjacent same-size blocks into their shared parent prefix (two /24s that together form a clean /23). The script prints both the collapsed block count and the address total, and separately reports how much of the RAW input sum was pure overlap, so you can see exactly what shrank and why.
Malformed lines are collected as errors WITH their line number rather than aborting the whole run — a single typo in a 10,000-line prefix export shouldn't cost you the other 9,999 lines' results. Blank lines and lines starting with "#" are silently treated as comments/whitespace, not errors, so this script accepts the same lightly-annotated prefix-list files people actually keep in version control.
This script makes no network request and has no API equivalent — the whole task is local arithmetic/parsing over data you already have, and there is nothing for our (or any) API to answer here. If you also need live IP facts (geolocation, ASN, threat/fraud signals), that's what the full API docs cover:
Like this tool? Our API gives free accounts 1,000 requests/day.See the API docs →