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.
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")
random.pySave 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
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
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.
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.
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}")
argparse?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.
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.