Skip to content

Random Port Generator — Python source

Generate one or many random TCP/UDP port numbers across registered, ephemeral, or the full range - optionally unique. Runs entirely in your browser with crypto-grade randomness.

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

"""random-port-generator — random/dynamic port picker over well-known ranges.

Language: Python (3.9+, standard library only)
Source:   CosmoDev polyglot showcase port of the Random Port Generator tool,
          ported from src/lib/random-port.ts (the canonical TypeScript
          implementation) and kept in lock-step with cli/random-port-generator
          (the Go twin). Pure + deterministic via an injectable RNG; invalid
          custom ranges raise ``ValueError`` (mirroring the TS throw).
License:  display source — part of CosmoDev's polyglot tool pages.

Design goals:
  - Pure + deterministic; an injectable RNG makes generation reproducible.
  - Functionally equivalent to the TS reference and the Go twin: same named
    ranges, same validation, same Fisher–Yates partial-shuffle unique pick.
  - Self-contained: stdlib only. The default RNG is ``secrets.random`` (a
    CSPRNG, matching the TS ``crypto.getRandomValues`` default).

The injectable ``rng`` is the key that makes a *random* tool deterministic and
therefore testable: every showcase assertion under ``__main__`` uses a fixed
lambda and reproduces a vector from src/lib/random-port.test.ts exactly.
"""

from __future__ import annotations

import secrets
from dataclasses import dataclass
from typing import Callable, List, Literal, Optional, Tuple

__all__ = ["PortRange", "PortOptions", "resolve_range", "random_port", "random_ports"]

PortRange = Literal["any", "registered", "ephemeral", "custom"]

# Named ranges mirror RANGES in random-port.ts (and the Go bounds() switch).
_RANGES: dict[str, tuple[int, int]] = {
    "any": (1, 65535),
    "registered": (1024, 49151),
    "ephemeral": (49152, 65535),
}


def _default_rng() -> float:
    """CSPRNG float in [0, 1) — the Python twin of TS's crypto-backed default."""
    return secrets.random()


@dataclass
class PortOptions:
    """Options mirror the TS ``PortOptions`` interface.

    Every field defaults to the TS default, so callers can construct the
    object incrementally with keyword args (a dataclass gives us that for free
    and keeps the option set self-documenting).
    """

    range: PortRange = "registered"
    min: Optional[int] = None  # custom-range lower bound (TS: ``min ?? 1``)
    max: Optional[int] = None  # custom-range upper bound (TS: ``max ?? 65535``)
    count: int = 1  # how many ports random_ports returns
    unique: bool = False  # dedupe via Fisher–Yates partial shuffle
    rng: Optional[Callable[[], float]] = None  # injectable random in [0, 1)


def resolve_range(opts: PortOptions) -> Tuple[int, int]:
    """Resolve an options object to its ``(lo, hi)`` bounds."""
    if opts.range == "custom":
        return (opts.min if opts.min is not None else 1,
                opts.max if opts.max is not None else 65535)
    return _RANGES[opts.range]


def _assert_range(lo: int, hi: int) -> None:
    """Mirror TS ``assertRange``: raise ValueError on out-of-bounds/inverted ranges."""
    if (not isinstance(lo, int) or not isinstance(hi, int)
            or lo < 0 or lo > 65535 or hi > 65535 or lo > hi):
        raise ValueError(f"Invalid port range {lo}-{hi}")


def random_port(opts: Optional[PortOptions] = None) -> int:
    """Return a single random port within the resolved range."""
    if opts is None:
        opts = PortOptions()
    rng = opts.rng or _default_rng
    lo, hi = resolve_range(opts)
    _assert_range(lo, hi)
    return lo + int(rng() * (hi - lo + 1))


def random_ports(opts: Optional[PortOptions] = None) -> List[int]:
    """Return ``count`` ports; when ``unique``, a Fisher–Yates partial shuffle
    over the range yields distinct values (capped at range capacity)."""
    if opts is None:
        opts = PortOptions()
    count = max(1, opts.count)
    lo, hi = resolve_range(opts)
    _assert_range(lo, hi)
    if not opts.unique:
        return [random_port(opts) for _ in range(count)]

    # Fisher–Yates partial shuffle over the range to pick `n` unique ports.
    capacity = hi - lo + 1
    n = min(count, capacity)
    rng = opts.rng or _default_rng
    pool = list(range(lo, hi + 1))
    for i in range(n):
        j = i + int(rng() * (capacity - i))
        pool[i], pool[j] = pool[j], pool[i]
    return pool[:n]


if __name__ == "__main__":
    # Showcase vectors mirror the deterministic cases in src/lib/random-port.test.ts.
    assert random_port(PortOptions(rng=lambda: 0.0)) == 1024
    assert random_port(PortOptions(range="any", rng=lambda: 0.0)) == 1
    assert random_port(PortOptions(range="ephemeral", rng=lambda: 0.0)) == 49152
    assert resolve_range(PortOptions()) == (1024, 49151)
    assert random_port(PortOptions(range="custom", min=8000, max=8000, rng=lambda: 0.9)) == 8000
    assert resolve_range(PortOptions(range="custom")) == (1, 65535)
    assert random_ports(PortOptions(count=5, rng=lambda: 0.0)) == [1024, 1024, 1024, 1024, 1024]
    uniq = random_ports(PortOptions(count=5, unique=True, range="custom",
                                    min=1, max=3, rng=lambda: 0.5))
    assert len(uniq) == 3 and sorted(uniq) == [1, 2, 3]
    try:
        random_port(PortOptions(range="custom", min=100, max=50))
        raise AssertionError("expected ValueError")
    except ValueError:
        pass
    print("ok")

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 →