Skip to content

Subnet / CIDR Calculator — Python source

Compute IPv4 subnet details from a CIDR block - network, broadcast, host range, netmask, wildcard, host count, IP class, and private-range detection. Split a block into smaller subnets, all in your browser.

This is the Python implementation — the same logic the interactive tool runs, in a shareable, citable form.

"""IPv4 subnet / CIDR math: dotted-quad ⇄ int conversion, full subnet details
(network, broadcast, hosts, class, privacy), and CIDR splitting into smaller
blocks. The logic is pure and deterministic; validation failures are reported
via ``None`` (or ``[]`` for the splitter) so callers can render a graceful
error instead of catching an exception.

Language: Python
CosmoDev polyglot showcase port of ``subnet-cidr``.
Ported from src/lib/subnet-cidr.ts — functionally equivalent.

Display source — part of CosmoDev's polyglot tool pages.
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Literal, Optional

# IPv4 addresses are manipulated as unsigned 32-bit integers. Python ints are
# arbitrary precision, so every bitwise result that must live in the IPv4 space
# is masked back into 32 bits with ``& 0xFFFFFFFF`` — the equivalent of
# JavaScript's ``>>> 0``. Without it, ``~mask`` would be a negative
# two's-complement value (Python models integers as sign-magnitude, so there is
# no implicit wraparound to undo the NOT).

# A whole-string digit check: rejects whitespace, signs, hex, and the empty
# string, matching the TypeScript ``/^\d+$/`` test.
_DIGITS = re.compile(r"\d+")


@dataclass
class SubnetInfo:
    """Computed properties of a CIDR block."""

    network: str
    broadcast: str
    first_host: str
    last_host: str
    netmask: str
    wildcard: str
    host_count: int
    prefix: int
    ip_class: Literal["A", "B", "C", "-"]
    is_private: bool


@dataclass
class Subnet:
    """One block produced by :func:`split_subnet`."""

    network: str
    prefix: int


def ip_to_int(ip: str) -> Optional[int]:
    """Convert a dotted-quad IPv4 string to an unsigned 32-bit integer.

    Returns ``None`` if the string is malformed or any octet exceeds 255.
    """
    parts = ip.split(".")
    if len(parts) != 4:
        return None
    result = 0
    for part in parts:
        # Only plain decimal digits — no whitespace, signs, or hex.
        if not _DIGITS.fullmatch(part):
            return None
        octet = int(part)
        if octet > 255:
            return None
        result = result * 256 + octet
    return result & 0xFFFFFFFF


def int_to_ip(n: int) -> str:
    """Convert an unsigned 32-bit integer to a dotted-quad IPv4 string."""
    v = n & 0xFFFFFFFF
    return f"{(v >> 24) & 255}.{(v >> 16) & 255}.{(v >> 8) & 255}.{v & 255}"


def parse_cidr(cidr: str) -> Optional[tuple[str, int]]:
    """Parse ``"ip/prefix"`` into a normalized ``(ip, prefix)`` pair.

    The prefix is constrained to ``[0, 32]``. Returns ``None`` if invalid.
    """
    if "/" not in cidr:
        return None
    # partition splits on the FIRST slash, so any second slash lands in the
    # prefix half — which we then reject.
    ip_part, _, prefix_part = cidr.partition("/")
    if "/" in prefix_part:  # multiple slashes ("a.b.c.d/24/16")
        return None
    if not _DIGITS.fullmatch(prefix_part):
        return None
    prefix = int(prefix_part)
    if prefix > 32:
        return None
    ip_int = ip_to_int(ip_part)
    if ip_int is None:
        return None
    # Re-emit through int_to_ip so the address is in canonical form.
    return (int_to_ip(ip_int), prefix)


def _mask_from_prefix(prefix: int) -> int:
    """Return the 32-bit netmask for a prefix length in ``[0, 32]``.

    The ``prefix == 0`` case is guarded for parity with the TS source; in
    Python ``0xFFFFFFFF << 32`` is a valid bignum whose low 32 bits are zero,
    but the explicit branch keeps the intent plain.
    """
    if prefix == 0:
        return 0
    return (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF


def subnet_details(cidr: str) -> Optional[SubnetInfo]:
    """Compute the full properties of a CIDR block.

    Returns ``None`` if the CIDR is invalid.
    """
    parsed = parse_cidr(cidr)
    if parsed is None:
        return None
    ip, prefix = parsed

    mask = _mask_from_prefix(prefix)
    wildcard = (~mask) & 0xFFFFFFFF
    ip_int = ip_to_int(ip)  # safe: parse_cidr validated and normalized the IP
    network = (ip_int & mask) & 0xFFFFFFFF
    broadcast = (network | wildcard) & 0xFFFFFFFF

    # /31 (point-to-point) and /32 (single host) have no host/broadcast split.
    if prefix >= 31:
        first_host = network
        last_host = broadcast
    else:
        first_host = (network + 1) & 0xFFFFFFFF
        last_host = (broadcast - 1) & 0xFFFFFFFF

    # Host count: 1 for /32, 2 for /31, otherwise 2^(32-prefix) − 2. Python
    # ints are unbounded, so the /0 case (2^32 − 2 = 4_294_967_294) is exact.
    if prefix >= 32:
        host_count = 1
    elif prefix == 31:
        host_count = 2
    else:
        host_count = (1 << (32 - prefix)) - 2

    first_octet = (network >> 24) & 255
    if first_octet < 128:
        ip_class: Literal["A", "B", "C", "-"] = "A"
    elif first_octet < 192:
        ip_class = "B"
    elif first_octet < 224:
        ip_class = "C"
    else:
        ip_class = "-"  # class D (multicast) / E (reserved)

    second_octet = (network >> 16) & 255
    is_private = (
        first_octet == 10
        or (first_octet == 172 and 16 <= second_octet <= 31)
        or (first_octet == 192 and second_octet == 168)
    )

    return SubnetInfo(
        network=int_to_ip(network),
        broadcast=int_to_ip(broadcast),
        first_host=int_to_ip(first_host),
        last_host=int_to_ip(last_host),
        netmask=int_to_ip(mask),
        wildcard=int_to_ip(wildcard),
        host_count=host_count,
        prefix=prefix,
        ip_class=ip_class,
        is_private=is_private,
    )


def split_subnet(cidr: str, new_prefix: int) -> list[Subnet]:
    """Split a CIDR block into smaller subnets of ``new_prefix``.

    Returns ``[]`` if the CIDR is invalid or ``new_prefix`` is outside
    ``(prefix, 32]``. The base IP is normalized to the network address before
    splitting.
    """
    parsed = parse_cidr(cidr)
    if parsed is None:
        return []
    ip, prefix = parsed
    # new_prefix is statically an int, so only the range is checked (mirrors the
    # TS Number.isInteger + bounds check).
    if new_prefix < prefix or new_prefix > 32:
        return []

    mask = _mask_from_prefix(prefix)
    network = (ip_to_int(ip) & mask) & 0xFFFFFFFF
    count = 1 << (new_prefix - prefix)  # 2^(new_prefix − prefix)
    block_size = 1 << (32 - new_prefix)  # 2^(32 − new_prefix)

    out: list[Subnet] = []
    for i in range(count):
        # The final & 0xFFFFFFFF makes addition wrap like a uint32.
        offset = (i * block_size) & 0xFFFFFFFF
        out.append(
            Subnet(network=int_to_ip((network + offset) & 0xFFFFFFFF), prefix=new_prefix)
        )
    return out

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →