Python Random Number Generator

A small, self-contained script that produces a configurable batch of unique random integers within a user-supplied range — every value in the returned list is guaranteed distinct. Output can be printed to stdout or written to a file. The script is written so the same code path works for tiny ranges (1–1000) and for ranges spanning quadrillions of values, without ever materialising the full range in memory.


The Script

The two public functions are random_numbers(start, end, count) and write_to_file(numbers, filename). Everything below if __name__ == "__main__": is a tiny argument parser so the script doubles as a CLI tool. Defaults are tuned for the common case: 200 unique random integers between 1_000_000 and 2_000_000, printed to stdout.
#!/usr/bin/env python
import random
import sys

def random_numbers(start, end, count=200):
    """
    Return a list of unique random integers between `start` and `end` (inclusive).

    Args:
        start (int):  Lower bound (inclusive)
        end (int):    Upper bound (inclusive)
        count (int):  How many numbers to return (default: 200)

    Returns:
        list: List of unique random integers
    """
    if count > (end - start + 1):
        raise ValueError(
            f"Cannot generate {count} unique numbers in range "
            f"[{start}, {end}] (only {end - start + 1} values available)"
        )
    return random.sample(range(start, end + 1), count)


def write_to_file(numbers, filename):
    """Write numbers to a file, one per line."""
    with open(filename, "w") as f:
        for n in numbers:
            f.write(f"{n}\n")
    print(f"Wrote {len(numbers)} numbers to {filename}")


if __name__ == "__main__":
    # Defaults
    start = 1_000_000
    end = 2_000_000
    count = 200
    output_file = None

    # Simple CLI: python script.py [start] [end] [count] [--out FILE]
    args = sys.argv[1:]
    positional = []
    i = 0
    while i < len(args):
        if args[i] == "--out" and i + 1 < len(args):
            output_file = args[i + 1]
            i += 2
        else:
            positional.append(args[i])
            i += 1

    if len(positional) >= 1:
        start = int(positional[0])
    if len(positional) >= 2:
        end = int(positional[1])
    if len(positional) >= 3:
        count = int(positional[2])

    numbers = random_numbers(start, end, count)

    if output_file:
        write_to_file(numbers, output_file)
    else:
        for n in numbers:
            print(n)
        print(f"\nTotal: {len(numbers)} unique numbers")


Don't Name the File random.py

Save this script as anything except random.py — for example random_numbers.py or gen.py. If the file is named random.py, the import random at the top imports your own file instead of the standard library, and random.sample() fails with:

AttributeError: module 'random' has no attribute 'sample'

The same trap applies to any local file named after a stdlib module — email.py, csv.py, queue.py, json.py, logging.py, etc. Python searches the current directory first, so a same-named local file always wins. If you've already hit this error, rename the file and delete the cached bytecode beside it:

mv random.py random_numbers.py
rm -rf __pycache__
./random_numbers.py


Usage

The CLI accepts up to three positional arguments (start, end, count) and one flag (--out FILE). Anything you omit falls back to the defaults baked into the script. Every run produces a list with no repeated values.
# Default: 200 unique numbers between 1_000_000 and 2_000_000, printed to stdout
./script.py

# Custom range and count
./script.py 1 1000 50

# Larger range, larger sample
./script.py 1000000 2000000 500

# Write to a file instead of printing
./script.py 1000000 2000000 200 --out numbers.txt

# Big sample written to a file
./script.py 1 1000000 5000 --out numbers.txt


How It Works

The single source of randomness is random.sample(), which performs sampling without replacement. The key trick is the first argument:
return random.sample(range(start, end + 1), count)

range(start, end + 1) is a lazy sequence object, not a materialised list. Python never allocates the full end - start + 1 integers in memory; range only stores start, stop, and step. random.sample is sequence-aware and uses random indexing into the range object, which means the same call works whether the range covers a thousand values or a quadrillion. Compare with the naive alternative random.sample(list(range(start, end + 1)), count) — that version would attempt to allocate the entire range as a Python list, and would OOM the process for any range above ~108.


Guard Against Impossible Requests

Asking for more unique values than the range can supply is a programming error, not a runtime condition to silently paper over. The guard at the top of the function raises ValueError with a message that names both the requested count and the actual range size:
if count > (end - start + 1):
    raise ValueError(
        f"Cannot generate {count} unique numbers in range "
        f"[{start}, {end}] (only {end - start + 1} values available)"
    )

Without this guard, random.sample would raise its own less-specific ValueError: Sample larger than population or is negative. The custom message is easier to act on when the call is buried three layers deep in a pipeline.


File Output

write_to_file writes one integer per line using a context manager, so the file is always closed cleanly even if the iteration raises. The final print is a small but useful piece of feedback for shell pipelines and cron jobs — you can confirm at a glance how many lines were written without re-running wc -l.
def write_to_file(numbers, filename):
    """Write numbers to a file, one per line."""
    with open(filename, "w") as f:
        for n in numbers:
            f.write(f"{n}\n")
    print(f"Wrote {len(numbers)} numbers to {filename}")


Why Not argparse?

For a script with three positional arguments and one flag, the hand-rolled parser is roughly the same number of lines as the equivalent argparse.ArgumentParser setup, but with no import overhead and no help-text boilerplate. Once a script grows past four or five options, switch to argparse — you get --help, type coercion, and validation for free. The parser here is deliberately positioned at the boundary where the trade-off still favours simplicity.


Reproducibility

The script uses the module-level random generator, which is seeded from OS entropy at import time. Every run therefore produces a different sequence. If you need the script to be deterministic — for tests, golden files, or replayable simulations — seed the generator before calling random_numbers:
import random
random.seed(42)
numbers = random_numbers(1_000_000, 2_000_000, 200)

The same seed will always produce the same list. For cryptographically secure randomness (tokens, keys, salts), random is the wrong module entirely — use secrets.randbelow instead.