CIDR List Aggregator Script (Python)

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.

How to run this script

placeholder

  1. Download cidr-aggregate.py below.
  2. Prepare a text file with one CIDR block per line (IPv4 and IPv6 may be mixed).
  3. Run: python3 cidr-aggregate.py prefixes.txt
  4. Read the printed per-family summary: input count, collapsed count, and total addresses covered.

Full source code

cidr-aggregate.py Download cidr-aggregate.py
#!/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.

CIDR List Aggregator Script (Python)

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.

Glossary

collapse_addresses
Python's ipaddress standard-library function that merges a list of networks into the smallest equivalent set of non-overlapping CIDR blocks — the entire aggregation logic in this script is one call to it.
strict=False
The ipaddress.ip_network option that accepts a prefix with host bits set (e.g. 10.1.2.3/24) and normalizes it to the network address, instead of raising an error.
Adjacent blocks
Two same-size CIDR blocks whose addresses are contiguous and together exactly fill their shared one-bit-shorter parent prefix (e.g. 10.0.0.0/24 + 10.0.1.0/24 = 10.0.0.0/23).
Redundant overlap
A smaller block fully contained within a larger one already in the same list — contributes zero new addresses and disappears entirely after collapsing.
num_addresses
An ipaddress network object's own address-count property — a /24 is 256, a /32 is 1 — summed here to report total coverage before and after collapsing.
Prefix list
A plain text export of CIDR blocks (e.g. an ACL, an allow-list, a BGP filter's input) — the raw material this recipe and geofeed-parse.py in this section both operate on.

API equivalent

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:

Read the full API docs

Frequently asked questions

Does collapsing change which addresses are covered?
No — the covered address set is mathematically identical before and after; collapsing only changes how many CIDR blocks it takes to express that same set.
Can I mix IPv4 and IPv6 in the same input file?
Yes — the script buckets them by family automatically and prints a separate summary for each; they are never compared or merged against each other.
What happens with a malformed line, like a typo in an octet?
It's collected into an error list with its line number and printed to stderr; the rest of the file is still processed normally.
Why would I aggregate a CIDR list instead of just using it as-is?
Firewall/ACL/BGP-filter rule counts often have hard limits, and a smaller, deduplicated rule set is both faster to evaluate and easier for a human to audit than a redundant one with overlapping entries.
Does this recipe touch the ip.xakki.pro API at all?
No, not at all — see the "API equivalent" section above for why: this is pure local set arithmetic over a list you already have, with nothing for an API to look up.

Like this tool? Our API gives free accounts 1,000 requests/day.See the API docs →