PythonRNGs.jl

Random numbers in Julia, generated by Python.

This package is for porting Python code, not for speed

It is not a faster random number generator — sampling calls into Python, so it is slower than native Julia RNGs. Its purpose is to help port existing Python libraries to Julia: because it reproduces Python's draws exactly, a translated routine can be validated against the original Python implementation value-for-value.

PythonRNGs exposes Python's random module and NumPy's two random APIs as Julia AbstractRNG subtypes through PythonCall.jl. It is intended for reproducing draws from Python inside Julia: floats, integers, integer ranges, and discrete ranges return exactly the values the corresponding Python generator would return for the same operation (see Reproducibility for the few values that are derived rather than taken verbatim from Python).

Standard normal draws, permutations, and vector shuffles also delegate to the Python backends, preserving their random-stream consumption and normal caches.

Three backends are provided:

BackendPython API
PythonRandomrandom.Random
NumPyRandomDefaultRNGnumpy.random.default_rng (Generator)
NumPyRandomlegacy numpy.random.RandomState (np.random.seed / np.random.random)

Installation

Install the registered package from the General registry:

using Pkg
Pkg.add("PythonRNGs")

Requirements:

  • Julia 1.10 or newer.

  • A Python interpreter with NumPy, for the NumPyRandomDefaultRNG and NumPyRandom backends. PythonCall.jl manages one with CondaPkg.jl; install NumPy with:

    using CondaPkg
    CondaPkg.add("numpy")

    PythonRandom needs only the Python standard library.

Usage

using PythonRNGs, Random

rng = PythonRandom(1234)              # wraps Python's random.Random(1234)
rand(rng)                             # 0.9664535356921388

default = NumPyRandomDefaultRNG(1234) # wraps numpy.random.default_rng(1234)
rand(default)                         # 0.9766997666981422

legacy = NumPyRandom(999)             # wraps numpy.random.RandomState(999)
rand(legacy)                          # 0.8034280400796879

All three are ordinary AbstractRNGs, so the usual Random entry points work:

rand(rng, Float64, 3)             # 3 uniform values in [0, 1)
rand(rng, Float32)                # uniform Float32 in [0, 1)
rand(rng, 1:6)                    # uniform integer (== random.choice([1,2,3,4,5,6]))
rand(rng, Bool)                   # uniform Bool
rand(rng, 10)                     # 10-element Vector{Float64} in [0, 1)
rand(rng, 1.0:0.5:2.0)            # uniform element of a discrete float range
rand(rng, 'a':'z')                # uniform character (== random.choice("abc..."))
rand(rng, Float64, 2, 2)          # 2×2 matrix
rand!(rng, zeros(3))              # fill an existing array
randn(rng)                       # native standard normal draw
randn(rng, 2, 3)                 # normal matrix in C order
randn!(rng, zeros(2, 3))          # fill an existing matrix in C order
randperm(rng, 5)                  # Python permutation, shifted to 1:5
shuffle(rng, [1, 2, 3, 4])        # shuffled copy using the backend
shuffle!(rng, [1, 2, 3, 4])       # shuffle a vector in place

Seeding

rng = PythonRandom(42)
rand(rng)
Random.seed!(rng, 42)              # reseed: reproduces the stream
Random.seed!(rng)                  # reseed from OS entropy

Testing

using Pkg
Pkg.test("PythonRNGs")

The test suite verifies, for all backends, that floats, integers, ranges (including step, Char, and discrete float ranges), and array draws reproduce the corresponding Python calls draw-for-draw, that unsupported ranges raise NotSupportedError, and that scalar types, reseeding, copying, and independence behave as expected. NumPy is provided to the test environment through CondaPkg (CondaPkg.toml at the repository root).