python-random
v1.0.0
Published
CPython's random module in TypeScript, bit for bit: the same seed draws the same numbers as Python's random.Random.
Maintainers
Readme
python-random
CPython's random module in TypeScript, bit for bit. Seed it the way you seed
Python and it draws the same numbers, in the same order, from every method.
>>> import random
>>> r = random.Random(42)
>>> r.random()
0.6394267984578837import { Random } from "python-random";
const r = new Random(42);
console.log(r.random()); // 0.6394267984578837Why
Math.random() cannot be seeded, and the seedable generators on npm are not
the one Python uses. When a Python service and a JavaScript one must agree on a
seeded draw, such as the same shuffle, the same sample or the same simulated
identity, both sides need the same generator and the same methods on top of it.
This package is a port of both: the Mersenne Twister (MT19937) core of
CPython's Modules/_randommodule.c, seeded exactly as CPython seeds it, and
the methods of Lib/random.py, with the same rejection loops, branch points and
floating-point evaluation order.
Install
npm install python-randomNode 20 or newer. ESM only, with TypeScript types included. No runtime dependencies.
Usage
import { Random } from "python-random";
const r = new Random(42);
console.log(r.random()); // 0.6394267984578837
console.log(r.randint(1, 6)); // 1
console.log(r.choice(["rock", "paper", "scissors"])); // scissors
console.log(r.sample(Array.from({ length: 100 }, (_, i) => i), 5)); // [ 35, 31, 28, 17, 94 ]
const deck = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
r.shuffle(deck);
console.log(deck); // [ 7, 2, 5, 6, 0, 3, 4, 9, 8, 1 ]
console.log(r.choices(["a", "b", "c"], { weights: [5, 1, 1], k: 6 })); // [ 'a', 'a', 'a', 'b', 'a', 'a' ]The same calls in Python print the same values:
r = random.Random(42)
r.random(); r.randint(1, 6); r.choice(["rock", "paper", "scissors"])
r.sample(range(100), 5)
deck = list(range(10)); r.shuffle(deck)
r.choices(["a", "b", "c"], [5, 1, 1], k=6)The module-level functions share one hidden generator, as Python's do:
import * as random from "python-random";
random.seed("hello");
console.log(random.random()); // 0.3537754404730722
console.log(random.randrange(0, 100, 5)); // 85Python ints have no size limit and JavaScript numbers stop being exact at
253, so integers past that are bigints. getrandbits() always
returns one; randrange() and randint() return whichever type they are
given:
import { Random } from "python-random";
const r = new Random(2n ** 100n);
console.log(r.getrandbits(100)); // 97103624483852547665086082069n
console.log(r.randrange(10n ** 30n)); // 881832314501477294104966971409n
console.log(r.randbytes(4)); // Uint8Array(4) [ 50, 9, 107, 212 ]A number that is not a safe integer is rejected with a RangeError rather than
rounded, since a rounded number is a different seed or range from the one
Python would see.
API
Everything is a method of Random, and each method is also exported as a
module-level function bound to one shared instance.
| JavaScript | Python |
|---|---|
| new Random(a?) | random.Random(a=None) |
| seed(a?) | seed(a=None) (version 2) |
| random() | random() |
| getrandbits(k), a bigint | getrandbits(k) |
| randbytes(n), a Uint8Array | randbytes(n) |
| randrange(start, stop?, step?) | randrange(start, stop=None, step=1) |
| randint(a, b) | randint(a, b) |
| choice(seq) | choice(seq) |
| choices(population, { weights, cumWeights, k }) | choices(population, weights=None, *, cum_weights=None, k=1) |
| sample(population, k, { counts }) | sample(population, k, *, counts=None) |
| shuffle(x), in place | shuffle(x) |
| uniform(a, b) | uniform(a, b) |
| triangular(low?, high?, mode?) | triangular(low=0.0, high=1.0, mode=None) |
A seed is a number (a safe integer), a bigint, a string, a Uint8Array (for
Python bytes), or null/undefined for OS entropy. Like Python, an int seed
uses its absolute value, and a string or bytes seed is hashed with SHA-512
(seed version 2).
A Python ValueError or IndexError is a RangeError here, and a TypeError
is a TypeError.
Not included
The distributions built on log, exp, sin, cos or pow (gauss,
normalvariate, lognormvariate, expovariate, vonmisesvariate,
gammavariate, betavariate, paretovariate, weibullvariate and
binomialvariate) are left out. V8 computes those functions with different
code from the C library CPython calls, so their results can differ in the last
bit, and this package only ships what it can match exactly. Also left out are
getstate()/setstate(), SystemRandom, seed version 1, and float seeds,
which Python hashes.
Guarantees
Every method above is checked against CPython 3.10, 3.11, 3.12, 3.13 and 3.14
on Node 20, 22 and 24. The tests run scripts/golden.py under a real
interpreter on every run, replay its calls over 22 seeds of every kind, and
compare each result exactly, along with which calls raise.
The versions agree on every result, with one exception in error handling:
sample(population, 0, { counts }) with counts that sum to zero returns []
here, as in Python 3.12 and later. Python 3.10 and 3.11 raise.
Development
pnpm install
pnpm check # biome
pnpm typecheck
pnpm build
pnpm test # needs CPython 3.10+: $PYTHON, else python3The tests regenerate test/fixtures/ from CPython on every run, and fail when
no interpreter is found. Matching CPython beats improving on it: a change that
alters which words are drawn, or in what order, is a bug even when it looks
like a cleanup.
Licence
PSF-2.0 AND BSD-3-Clause. The methods are derived from CPython's
Lib/random.py (Python Software Foundation License Version 2) and the
generator from the MT19937 code in Modules/_randommodule.c (© Makoto
Matsumoto and Takuji Nishimura, BSD 3-clause). The port itself is © Jake Writer
under BSD 3-clause. LICENSE holds all three texts, and
NOTICE summarises the changes from CPython.
