You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
DISCUSS: Migration path from legacy RandomState to NumPy Generator #877
Our RNG plumbing is the 2015-era scikit-learn idiom: check_random_state (util/random.py) normalizes to np.random.RandomState, reaches into np.random.mtrand._rand for the global instance, and rng_integers exists solely to bridge RandomState/Generator signature differences. NumPy has recommended Generator/default_rng since 1.17 (2019): the actively developed API, faster bit generators, better streams. We accept Generator today, but every internal default and documented example takes the legacy path.
This is a decision, not a patch, because reproducibility is the product: for integer seeds, default_rng(seed) and RandomState(seed) produce different streams, so a silent default switch changes every seeded simulation result in downstream research and the QuantEcon lectures.
Questions for this thread
Do we migrate defaults at all, or only document Generator as the preferred input?
If we migrate: flag-day at a major-ish version with a loud changelog, or a transition period where integer seeds emit a FutureWarning naming the change date?
What do the lecture repositories need from us (coordination, pinned examples)?
Does check_random_state grow a default="generator" parameter, or do we adopt np.random.default_rng passthrough plus spawn for parallel paths?
Suggested outcome
A short policy (target version, warning strategy, docs plan) recorded here, then this thread closes and spawns the implementation issue. Related: the vendoring of rng_integers (this milestone) is worth doing first regardless; a completed migration eventually deletes the vendored copy too.
From the July 2026 technical-debt audit (AI-assisted; claims verified against 28d4b3b on 2026-07-25).
Some facts to frame this, then a concrete proposal on each of the four questions so the thread has something to agree with or reject rather than a blank page.
The surface.random_state= appears 182 times across 13 non-test modules — game_theory/ (brd, fictplay, localint, logitdyn, normal_form_game, random, game_generators/bimatrix_generators), markov/ (approximation, core, random), random/utilities.py, and util/ (random, __init__). Against that, the rng= spelling introduced by #917 exists on exactly one public function, random.draw.
One constraint that shapes everything else.draw(cdf, size, rng=None) resolves None to np.random — the legacy global module — so np.random.seed(1234); draw(cdf, 10) is reproducible today, and that idiom is used throughout the lectures. If None ever resolves to default_rng() instead, the seed silently stops controlling the draw. So None must keep meaning "the legacy global" until a deliberate flag day, independently of what we decide about explicit seeds.
1. Do we migrate defaults at all? Proposal: no — never change what an existing parameter means. The reproducibility problem exists entirely because random_state=1234 would begin producing a different stream. A new parameter avoids it completely: random_state= keeps its exact current semantics for its whole life, and rng=, accepting None or a Generator, becomes the modern spelling. Users move by editing code, so no published result ever moves under them. #917 already did this for draw; this is just making it the pattern.
2. Flag-day or FutureWarning? Proposal: neither, because with a new parameter name there is no stream change to warn about. What is needed is an ordinary deprecation of the old parameter: rng= added alongside random_state= with both working silently; random_state= gains a DeprecationWarning only once the lecture repositories have migrated; removal no earlier than v1.0. Note that 182 call sites is a far larger surface than the timer deprecations in #833 — switching the warning on early makes every lecture that seeds a simulation noisy, which is why the warning should follow the lecture migration rather than lead it.
3. What the lecture repositories need. Proposal: a window in which both spellings work with no warnings, so lectures migrate per-lecture rather than in one sweep; and explicit guidance that a seeded example whose published figures must reproduce should stay on random_state= until that lecture is re-run and its outputs regenerated. Practically, a tracking issue in the lecture repos before the warning is switched on, not after.
4. Does check_random_state grow a default= parameter? Proposal: no — do not grow it. Its contract is "give me a RandomState", and a mode flag makes every call site ambiguous about which type comes back. draw shows the alternative: accept None | Generator directly and resolve None locally, with no helper at all. If a helper does prove necessary for the rng= path it should be a separate small function with one job. spawn for parallel paths deserves its own issue — it is a capability we lack under either spelling, and bundling it here will stall this decision.
5. A fifth question this thread should absorb: the shape of rng_integers.#946 vendors SciPy's implementation to remove the private scipy._lib._util import (#868). That implementation hard-codes gen=None to np.random.mtrand._rand, which is precisely the choice question 1 above is about — so adopting it wholesale settles part of this policy by default. There is also a licensing wrinkle: QuantEcon.py is MIT and SciPy is BSD-3, so vendoring carries a licence block, where quantecon/util/compat.py takes a one-line attribution for code from the same SciPy file. Since the dispatch logic is dictated entirely by the two NumPy APIs, writing a small adapter of our own may be cleaner than vendoring, and lets us choose the None semantics deliberately. #946 is held pending this.
What this adds up to: a long, boring migration with no flag day and no changed streams — add rng= module by module, leave random_state= alone, warn only when the lectures are ready, remove at v1.0 or later. The cost is carrying two parameters for a while. The benefit is that no downstream result changes silently, which given this issue's own framing — "reproducibility is the product" — looks like the right trade.
If that is broadly agreeable, the implementation issue this thread spawns is "add rng= across the 13 modules", and #946 can proceed once question 5 is answered.
Activity
mmcky commentedon Sep 9, 2026
Some facts to frame this, then a concrete proposal on each of the four questions so the thread has something to agree with or reject rather than a blank page.
The surface.
random_state=appears 182 times across 13 non-test modules —game_theory/(brd, fictplay, localint, logitdyn, normal_form_game, random, game_generators/bimatrix_generators),markov/(approximation, core, random),random/utilities.py, andutil/(random,__init__). Against that, therng=spelling introduced by #917 exists on exactly one public function,random.draw.One constraint that shapes everything else.
draw(cdf, size, rng=None)resolvesNonetonp.random— the legacy global module — sonp.random.seed(1234); draw(cdf, 10)is reproducible today, and that idiom is used throughout the lectures. IfNoneever resolves todefault_rng()instead, the seed silently stops controlling the draw. SoNonemust keep meaning "the legacy global" until a deliberate flag day, independently of what we decide about explicit seeds.1. Do we migrate defaults at all? Proposal: no — never change what an existing parameter means. The reproducibility problem exists entirely because
random_state=1234would begin producing a different stream. A new parameter avoids it completely:random_state=keeps its exact current semantics for its whole life, andrng=, acceptingNoneor aGenerator, becomes the modern spelling. Users move by editing code, so no published result ever moves under them. #917 already did this fordraw; this is just making it the pattern.2. Flag-day or
FutureWarning? Proposal: neither, because with a new parameter name there is no stream change to warn about. What is needed is an ordinary deprecation of the old parameter:rng=added alongsiderandom_state=with both working silently;random_state=gains aDeprecationWarningonly once the lecture repositories have migrated; removal no earlier than v1.0. Note that 182 call sites is a far larger surface than the timer deprecations in #833 — switching the warning on early makes every lecture that seeds a simulation noisy, which is why the warning should follow the lecture migration rather than lead it.3. What the lecture repositories need. Proposal: a window in which both spellings work with no warnings, so lectures migrate per-lecture rather than in one sweep; and explicit guidance that a seeded example whose published figures must reproduce should stay on
random_state=until that lecture is re-run and its outputs regenerated. Practically, a tracking issue in the lecture repos before the warning is switched on, not after.4. Does
check_random_stategrow adefault=parameter? Proposal: no — do not grow it. Its contract is "give me aRandomState", and a mode flag makes every call site ambiguous about which type comes back.drawshows the alternative: acceptNone | Generatordirectly and resolveNonelocally, with no helper at all. If a helper does prove necessary for therng=path it should be a separate small function with one job.spawnfor parallel paths deserves its own issue — it is a capability we lack under either spelling, and bundling it here will stall this decision.5. A fifth question this thread should absorb: the shape of
rng_integers. #946 vendors SciPy's implementation to remove the privatescipy._lib._utilimport (#868). That implementation hard-codesgen=Nonetonp.random.mtrand._rand, which is precisely the choice question 1 above is about — so adopting it wholesale settles part of this policy by default. There is also a licensing wrinkle: QuantEcon.py is MIT and SciPy is BSD-3, so vendoring carries a licence block, wherequantecon/util/compat.pytakes a one-line attribution for code from the same SciPy file. Since the dispatch logic is dictated entirely by the two NumPy APIs, writing a small adapter of our own may be cleaner than vendoring, and lets us choose theNonesemantics deliberately. #946 is held pending this.What this adds up to: a long, boring migration with no flag day and no changed streams — add
rng=module by module, leaverandom_state=alone, warn only when the lectures are ready, remove at v1.0 or later. The cost is carrying two parameters for a while. The benefit is that no downstream result changes silently, which given this issue's own framing — "reproducibility is the product" — looks like the right trade.If that is broadly agreeable, the implementation issue this thread spawns is "add
rng=across the 13 modules", and #946 can proceed once question 5 is answered.oyamad commentedon Sep 10, 2026
My thoughts:
drawis an exception; out of the scope here. Its restriction is a Numba constraint, not a design to follow in other functions.np.random.seed(1234)is a bad convention. I understand such idioms in the lectures are going to be removed.rngkeyword, and internally pass it tonumpy.random.default_rng(which also accepts an integer seed).