[{"data":1,"prerenderedAt":910},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests\u002Fseeding-random-and-numpy-for-reproducible-tests\u002F":3},{"id":4,"title":5,"body":6,"description":873,"extension":874,"meta":875,"navigation":96,"path":906,"seo":907,"stem":908,"__hash__":909},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests\u002Fseeding-random-and-numpy-for-reproducible-tests\u002Findex.md","Seeding random and NumPy for Reproducible Tests",{"type":7,"value":8,"toc":862},"minimark",[9,18,21,26,58,62,239,291,401,405,420,438,442,511,515,518,524,534,540,633,637,640,647,650,654,665,668,676,772,776,788,801,820,824,853,858],[10,11,12,13,17],"p",{},"A test that exercises randomised code — shuffling, sampling, jitter, simulated data — has two failure modes. If the randomness is not controlled, the test passes on most runs and fails on the one where the generator produced an edge case, and nobody can reproduce it. If the randomness is controlled with a single global ",[14,15,16],"code",{},"random.seed(42)",", the test is reproducible until someone adds a test before it that also draws random numbers, at which point every later test sees a different sequence and some of them fail for reasons unrelated to any change.",[10,19,20],{},"The robust arrangement has three parts: randomised code receives an explicit generator rather than calling module-level functions, each test derives its own seed from a printed session seed and its node id, and assertions check properties of the output rather than exact values wherever possible. With those in place, a failure caused by an unlucky draw is reproducible with one command and stays put when the suite changes around it.",[22,23,25],"h2",{"id":24},"prerequisites","Prerequisites",[27,28,29,41,50],"ul",{},[30,31,32,33,36,37,40],"li",{},"Python 3.9+; ",[14,34,35],{},"random"," in the standard library, NumPy 1.17+ for ",[14,38,39],{},"default_rng",".",[30,42,43,46,47,40],{},[14,44,45],{},"pytest >= 8.0",", and optionally ",[14,48,49],{},"pytest-randomly",[30,51,52,53,40],{},"The same approach for fake data is in ",[54,55,57],"a",{"href":56},"\u002Fintegration-database-and-service-testing\u002Ftest-data-factories-and-builders\u002Fgenerating-reproducible-fake-data-with-faker\u002F","generating reproducible fake data with Faker",[22,59,61],{"id":60},"solution","Solution",[63,64,69],"pre",{"className":65,"code":66,"language":67,"meta":68,"style":68},"language-python shiki shiki-themes github-light github-dark","import hashlib\nimport os\nimport random\n\nimport numpy as np\nimport pytest\n\n\n@pytest.fixture(scope=\"session\")\ndef session_seed(request):\n    seed = int(os.environ.get(\"TEST_SEED\", random.SystemRandom().randrange(2**32)))\n    print(f\"\\ntest session seed: {seed}  (re-run with TEST_SEED={seed})\")\n    return seed\n\n\ndef _derive(session_seed: int, nodeid: str) -> int:\n    digest = hashlib.sha256(f\"{session_seed}:{nodeid}\".encode()).digest()\n    return int.from_bytes(digest[:8], \"big\")\n\n\n@pytest.fixture\ndef rng(request, session_seed) -> random.Random:\n    # An explicit, per-test generator: order-independent and parallel-safe.\n    return random.Random(_derive(session_seed, request.node.nodeid))\n\n\n@pytest.fixture\ndef np_rng(request, session_seed) -> np.random.Generator:\n    return np.random.default_rng(_derive(session_seed, request.node.nodeid))\n","python","",[14,70,71,79,85,91,98,104,110,115,120,126,132,138,144,150,155,160,166,172,178,183,188,194,200,206,212,217,222,227,233],{"__ignoreMap":68},[72,73,76],"span",{"class":74,"line":75},"line",1,[72,77,78],{},"import hashlib\n",[72,80,82],{"class":74,"line":81},2,[72,83,84],{},"import os\n",[72,86,88],{"class":74,"line":87},3,[72,89,90],{},"import random\n",[72,92,94],{"class":74,"line":93},4,[72,95,97],{"emptyLinePlaceholder":96},true,"\n",[72,99,101],{"class":74,"line":100},5,[72,102,103],{},"import numpy as np\n",[72,105,107],{"class":74,"line":106},6,[72,108,109],{},"import pytest\n",[72,111,113],{"class":74,"line":112},7,[72,114,97],{"emptyLinePlaceholder":96},[72,116,118],{"class":74,"line":117},8,[72,119,97],{"emptyLinePlaceholder":96},[72,121,123],{"class":74,"line":122},9,[72,124,125],{},"@pytest.fixture(scope=\"session\")\n",[72,127,129],{"class":74,"line":128},10,[72,130,131],{},"def session_seed(request):\n",[72,133,135],{"class":74,"line":134},11,[72,136,137],{},"    seed = int(os.environ.get(\"TEST_SEED\", random.SystemRandom().randrange(2**32)))\n",[72,139,141],{"class":74,"line":140},12,[72,142,143],{},"    print(f\"\\ntest session seed: {seed}  (re-run with TEST_SEED={seed})\")\n",[72,145,147],{"class":74,"line":146},13,[72,148,149],{},"    return seed\n",[72,151,153],{"class":74,"line":152},14,[72,154,97],{"emptyLinePlaceholder":96},[72,156,158],{"class":74,"line":157},15,[72,159,97],{"emptyLinePlaceholder":96},[72,161,163],{"class":74,"line":162},16,[72,164,165],{},"def _derive(session_seed: int, nodeid: str) -> int:\n",[72,167,169],{"class":74,"line":168},17,[72,170,171],{},"    digest = hashlib.sha256(f\"{session_seed}:{nodeid}\".encode()).digest()\n",[72,173,175],{"class":74,"line":174},18,[72,176,177],{},"    return int.from_bytes(digest[:8], \"big\")\n",[72,179,181],{"class":74,"line":180},19,[72,182,97],{"emptyLinePlaceholder":96},[72,184,186],{"class":74,"line":185},20,[72,187,97],{"emptyLinePlaceholder":96},[72,189,191],{"class":74,"line":190},21,[72,192,193],{},"@pytest.fixture\n",[72,195,197],{"class":74,"line":196},22,[72,198,199],{},"def rng(request, session_seed) -> random.Random:\n",[72,201,203],{"class":74,"line":202},23,[72,204,205],{},"    # An explicit, per-test generator: order-independent and parallel-safe.\n",[72,207,209],{"class":74,"line":208},24,[72,210,211],{},"    return random.Random(_derive(session_seed, request.node.nodeid))\n",[72,213,215],{"class":74,"line":214},25,[72,216,97],{"emptyLinePlaceholder":96},[72,218,220],{"class":74,"line":219},26,[72,221,97],{"emptyLinePlaceholder":96},[72,223,225],{"class":74,"line":224},27,[72,226,193],{},[72,228,230],{"class":74,"line":229},28,[72,231,232],{},"def np_rng(request, session_seed) -> np.random.Generator:\n",[72,234,236],{"class":74,"line":235},29,[72,237,238],{},"    return np.random.default_rng(_derive(session_seed, request.node.nodeid))\n",[63,240,242],{"className":65,"code":241,"language":67,"meta":68,"style":68},"def sample_customers(customers, k, *, rng: random.Random):\n    return rng.sample(customers, k)          # randomness is a parameter, not a global\n\n\ndef test_sample_never_repeats_a_customer(rng):\n    customers = [f\"cus_{i}\" for i in range(100)]\n    chosen = sample_customers(customers, 10, rng=rng)\n\n    assert len(chosen) == len(set(chosen)) == 10     # a property, not exact values\n    assert set(chosen) \u003C= set(customers)\n",[14,243,244,249,254,258,262,267,272,277,281,286],{"__ignoreMap":68},[72,245,246],{"class":74,"line":75},[72,247,248],{},"def sample_customers(customers, k, *, rng: random.Random):\n",[72,250,251],{"class":74,"line":81},[72,252,253],{},"    return rng.sample(customers, k)          # randomness is a parameter, not a global\n",[72,255,256],{"class":74,"line":87},[72,257,97],{"emptyLinePlaceholder":96},[72,259,260],{"class":74,"line":93},[72,261,97],{"emptyLinePlaceholder":96},[72,263,264],{"class":74,"line":100},[72,265,266],{},"def test_sample_never_repeats_a_customer(rng):\n",[72,268,269],{"class":74,"line":106},[72,270,271],{},"    customers = [f\"cus_{i}\" for i in range(100)]\n",[72,273,274],{"class":74,"line":112},[72,275,276],{},"    chosen = sample_customers(customers, 10, rng=rng)\n",[72,278,279],{"class":74,"line":117},[72,280,97],{"emptyLinePlaceholder":96},[72,282,283],{"class":74,"line":122},[72,284,285],{},"    assert len(chosen) == len(set(chosen)) == 10     # a property, not exact values\n",[72,287,288],{"class":74,"line":128},[72,289,290],{},"    assert set(chosen) \u003C= set(customers)\n",[292,293,296,397],"figure",{"className":294},[295],"diagram",[297,298,305,306,305,310,305,314,305,322,305,332,305,342,305,348,305,354,305,358,305,362,305,367,305,371,305,376,305,380,305,384,305,387,305,390,305,394],"svg",{"viewBox":299,"role":300,"ariaLabelledBy":301,"xmlns":304},"0 0 820 262","img",[302,303],"sd-t","sd-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[307,308,309],"title",{"id":302},"Global seed versus per-test derived seeds",[311,312,313],"desc",{"id":303},"With one global seed, each test's random values depend on how many numbers earlier tests drew, so inserting a test changes the values every later test sees. With a seed derived from the session seed and the node id, each test gets an independent stream that is identical whatever runs before it, including under parallel workers.",[315,316],"rect",{"x":317,"y":317,"width":318,"height":319,"rx":320,"fill":321},"0","820","262","14","#fffdf8",[323,324,331],"text",{"x":325,"y":326,"textAnchor":327,"fontSize":328,"fontWeight":329,"fill":330},"410","28","middle","16","700","#3d405b","Where each test's random stream begins",[315,333],{"x":334,"y":335,"width":336,"height":337,"rx":338,"fill":339,"stroke":340,"strokeWidth":341},"26","52","368","186","12","#fbe9e3","#e07a5f","2",[323,343,347],{"x":344,"y":345,"textAnchor":327,"fontSize":346,"fontWeight":329,"fill":330},"210","78","12.5","random.seed(42) once",[323,349,353],{"x":350,"y":351,"fontSize":352,"fill":330},"44","108","11","test A draws numbers 1–5",[323,355,357],{"x":350,"y":356,"fontSize":352,"fill":330},"130","test B draws numbers 6–9",[323,359,361],{"x":350,"y":360,"fontSize":352,"fill":330},"152","add a test before B →",[323,363,366],{"x":350,"y":364,"fontSize":352,"fill":365},"174","#8f3d22","B's values all change",[323,368,370],{"x":350,"y":369,"fontSize":352,"fontWeight":329,"fill":365},"212","failures move when tests are added",[315,372],{"x":373,"y":335,"width":336,"height":337,"rx":338,"fill":374,"stroke":375,"strokeWidth":341},"426","#e6f0ea","#81b29a",[323,377,379],{"x":378,"y":345,"textAnchor":327,"fontSize":346,"fontWeight":329,"fill":330},"610","Random(hash(seed, nodeid))",[323,381,383],{"x":382,"y":351,"fontSize":352,"fill":330},"444","each test owns its generator",[323,385,386],{"x":382,"y":356,"fontSize":352,"fill":330},"independent of order",[323,388,389],{"x":382,"y":360,"fontSize":352,"fill":330},"independent of xdist worker",[323,391,393],{"x":382,"y":364,"fontSize":352,"fill":392},"#2a5f49","-k one_test reproduces exactly",[323,395,396],{"x":382,"y":369,"fontSize":352,"fontWeight":329,"fill":392},"a failure stays where it is",[398,399,400],"figcaption",{},"The right-hand arrangement also makes randomness visible in signatures, which is where a reader looks to understand why a function's output varies.",[22,402,404],{"id":403},"why-this-works","Why this works",[10,406,407,408,411,412,415,416,419],{},"A ",[14,409,410],{},"random.Random"," instance carries its own state. Seeding it affects nothing else in the process, and nothing else can advance it, so the sequence a test sees depends only on the seed it was created with. Deriving that seed by hashing the session seed with the node id gives every test a distinct, stable starting point: the same test always gets the same stream for a given session seed, regardless of which tests ran before it, whether it was selected alone with ",[14,413,414],{},"-k",", or which ",[14,417,418],{},"pytest-xdist"," worker executed it.",[10,421,422,423,425,426,429,430,433,434,437],{},"NumPy's ",[14,424,39],{}," returns a ",[14,427,428],{},"Generator"," with the same property, and it is the interface NumPy recommends for new code. The legacy ",[14,431,432],{},"np.random.seed"," controls a single global ",[14,435,436],{},"RandomState"," shared by everything in the process, which has exactly the ordering problem described above. It also means that a library you call — a data-augmentation routine, a sampling utility — can silently consume numbers from the same global stream, so the values your own code sees depend on what the library did first. Explicit generators remove that coupling entirely: the library gets its own generator, your code gets its own, and neither affects the other.",[22,439,441],{"id":440},"edge-cases-and-failure-modes","Edge cases and failure modes",[27,443,444,460,480,490,498],{},[30,445,446,452,453,456,457,459],{},[447,448,449,450,40],"strong",{},"Library code using module-level ",[14,451,35],{}," Code you cannot change that calls ",[14,454,455],{},"random.random()"," directly still reads global state. Seed the global generator per test as well — ",[14,458,49],{}," does this — as a fallback, not a substitute.",[30,461,462,469,470,473,474,476,477,40],{},[447,463,464,465,468],{},"Python's ",[14,466,467],{},"hash()"," for deriving seeds."," String hashing is salted per process unless ",[14,471,472],{},"PYTHONHASHSEED"," is set, so seeds derived with ",[14,475,467],{}," differ between runs and between workers. Use ",[14,478,479],{},"hashlib",[30,481,482,485,486,489],{},[447,483,484],{},"Exact-value assertions."," ",[14,487,488],{},"assert rng.random() == 0.6394…"," breaks when the algorithm or the library version changes. Prefer invariants: bounds, uniqueness, distribution properties.",[30,491,492,485,495,497],{},[447,493,494],{},"Threads sharing one generator.",[14,496,410],{}," is not safe for concurrent use without a lock. Give each thread its own instance, derived from the test's seed.",[30,499,500,485,503,506,507,510],{},[447,501,502],{},"Cryptographic randomness.",[14,504,505],{},"secrets"," and ",[14,508,509],{},"os.urandom"," are deliberately unseedable. Code that uses them for non-security purposes should take a generator instead; code that uses them for security should be tested by property, not by value.",[22,512,514],{"id":513},"asserting-on-randomised-output","Asserting on randomised output",[10,516,517],{},"The hardest part of testing randomised code is choosing what to assert. Pinning exact values makes the test a snapshot of one library version's algorithm; asserting nothing makes it useless. The middle ground is properties that hold for every valid output, and most randomised functions have several.",[10,519,520,523],{},[447,521,522],{},"Structural properties"," are the easiest: a sample has the requested size, contains no duplicates, draws only from the population; a shuffle is a permutation of its input; jittered delays fall within their bounds. These never depend on the seed and survive any change to the underlying algorithm.",[10,525,526,529,530,533],{},[447,527,528],{},"Statistical properties"," need more care but catch different bugs. Drawing ten thousand values from a generator that should be uniform on ",[14,531,532],{},"[0, 1)"," and checking the mean lies within a generous band around 0.5 catches an off-by-one that biases every draw. Seeding makes such a test deterministic — it either always passes or always fails for a given seed — and choosing a wide tolerance keeps it from failing on legitimate variation.",[10,535,536,539],{},[447,537,538],{},"Relational properties"," compare two runs: the same seed gives the same output, different seeds give different output. These pin down that randomness is actually wired through, which is the property that breaks when someone replaces an injected generator with a call to the module-level function.",[292,541,543,630],{"className":542},[295],[297,544,305,549,305,552,305,555,305,559,305,564,305,570,305,573,305,578,305,583,305,587,305,591,305,595,305,597,305,600,305,604,305,607,305,611,305,614,305,616,305,620,305,624,305,627],{"viewBox":545,"role":300,"ariaLabelledBy":546,"xmlns":304},"0 0 800 236",[547,548],"pr-t","pr-d",[307,550,551],{"id":547},"Three kinds of assertion for random output",[311,553,554],{"id":548},"Three cards. Structural properties such as size, uniqueness and bounds hold for every seed. Statistical properties such as a mean within a tolerance catch biased generators. Relational properties such as same-seed-same-output confirm the generator is actually wired through the code.",[315,556],{"x":317,"y":317,"width":557,"height":558,"rx":320,"fill":321},"800","236",[323,560,563],{"x":561,"y":326,"textAnchor":327,"fontSize":562,"fontWeight":329,"fill":330},"400","15.5","Assert what must hold, not what happened to come out",[315,565],{"x":566,"y":567,"width":568,"height":569,"rx":338,"fill":321,"stroke":375,"strokeWidth":341},"24","50","240","164",[315,571],{"x":566,"y":567,"width":568,"height":572,"rx":338,"fill":330},"30",[323,574,577],{"x":575,"y":576,"textAnchor":327,"fontSize":338,"fontWeight":329,"fill":321},"144","70","structural",[323,579,582],{"x":580,"y":581,"fontSize":352,"fill":330},"40","104","size, no duplicates,",[323,584,586],{"x":580,"y":585,"fontSize":352,"fill":330},"126","within bounds",[323,588,590],{"x":580,"y":589,"fontSize":352,"fill":392},"170","holds for every seed",[315,592],{"x":593,"y":567,"width":568,"height":569,"rx":338,"fill":321,"stroke":594,"strokeWidth":341},"280","#f2cc8f",[315,596],{"x":593,"y":567,"width":568,"height":572,"rx":338,"fill":330},[323,598,599],{"x":561,"y":576,"textAnchor":327,"fontSize":338,"fontWeight":329,"fill":321},"statistical",[323,601,603],{"x":602,"y":581,"fontSize":352,"fill":330},"296","mean in a wide band",[323,605,606],{"x":602,"y":585,"fontSize":352,"fill":330},"over many draws",[323,608,610],{"x":602,"y":589,"fontSize":352,"fill":609},"#8a5a00","catches biased code",[315,612],{"x":613,"y":567,"width":568,"height":569,"rx":338,"fill":321,"stroke":340,"strokeWidth":341},"536",[315,615],{"x":613,"y":567,"width":568,"height":572,"rx":338,"fill":330},[323,617,619],{"x":618,"y":576,"textAnchor":327,"fontSize":338,"fontWeight":329,"fill":321},"656","relational",[323,621,623],{"x":622,"y":581,"fontSize":352,"fill":330},"552","same seed, same output;",[323,625,626],{"x":622,"y":585,"fontSize":352,"fill":330},"new seed, new output",[323,628,629],{"x":622,"y":589,"fontSize":352,"fill":365},"proves the wiring",[398,631,632],{},"A randomised function with one test from each column is better covered than one with a dozen exact-value snapshots.",[22,634,636],{"id":635},"wiring-generators-through-an-application","Wiring generators through an application",[10,638,639],{},"Injecting a generator into one function is easy; doing it across an application needs a convention, or the parameter gets threaded through a dozen call sites by hand. The pattern that scales is the same one used for clocks: services receive a generator in their constructor, defaulting to a fresh unseeded instance, and pass it down to the functions they call.",[10,641,642,643,646],{},"In production nothing changes — each service creates its own generator, which is as random as the module-level functions and has no shared state. In tests the factory that builds services passes a generator derived from the test's seed, so every random decision the service makes is reproducible. And a reviewer reading a service's constructor sees immediately that its behaviour involves randomness, which is information a hidden ",[14,644,645],{},"random.choice"," deep in a helper never gives.",[10,648,649],{},"The convention also makes one class of bug impossible: two components accidentally sharing a generator and interfering with each other's sequences. Each owns its instance, derived from a common seed when a test needs determinism, and independent otherwise.",[22,651,653],{"id":652},"replaying-a-failure","Replaying a failure",[10,655,656,657,660,661,664],{},"The printed session seed is what turns a flaky-looking failure into a reproducible one. When CI reports a failure in ",[14,658,659],{},"test_sample_never_repeats_a_customer",", the log header carries the seed, and re-running locally with ",[14,662,663],{},"TEST_SEED=\u003Cvalue> pytest -k test_sample_never_repeats_a_customer"," produces exactly the same random values for exactly that test, because the derivation depends only on the seed and the node id.",[10,666,667],{},"That makes the investigation ordinary: run it, see it fail, add a print or a breakpoint, fix the code. Once fixed, the specific input that exposed the bug is worth preserving as an explicit, non-random test case — the seed will change on the next run, and a regression test that depends on reproducing a particular random draw is fragile. A plain test with the problematic input written out keeps the bug fixed regardless of future seeds.",[10,669,670,671,675],{},"For randomised code where finding such inputs matters — shuffles that must preserve some property, samplers that must respect constraints — property-based testing with Hypothesis is the stronger tool, because it searches for failing inputs deliberately and shrinks them to minimal cases, as covered in ",[54,672,674],{"href":673},"\u002Fproperty-based-fuzz-testing-strategies\u002F","property-based and fuzz testing strategies",". Seeded randomness in ordinary tests is for determinism; Hypothesis is for exploration.",[292,677,679,769],{"className":678},[295],[297,680,305,685,305,688,305,691,305,708,305,711,305,714,305,718,305,723,305,727,305,731,305,738,305,742,305,746,305,749,305,752,305,755,305,759,305,763,305,766],{"viewBox":681,"role":300,"ariaLabelledBy":682,"xmlns":304},"0 0 800 200",[683,684],"rp-t","rp-d",[307,686,687],{"id":683},"From a CI failure to a permanent regression test",[311,689,690],{"id":684},"A CI run fails and prints its session seed. Re-running the single test locally with that seed reproduces the exact random values. After the fix, the problematic input is written out as an explicit test case so the regression is guarded independently of any future seed.",[692,693,694,695,305],"defs",{},"\n    ",[696,697,704],"marker",{"id":698,"viewBox":699,"refX":700,"refY":701,"markerWidth":702,"markerHeight":702,"orient":703},"rp-a","0 0 10 10","9","5","7","auto-start-reverse",[705,706],"path",{"d":707,"fill":330},"M0 0 L10 5 L0 10 z",[315,709],{"x":317,"y":317,"width":557,"height":710,"rx":320,"fill":321},"200",[323,712,713],{"x":561,"y":326,"textAnchor":327,"fontSize":562,"fontWeight":329,"fill":330},"Seed to reproduce, explicit case to keep it fixed",[315,715],{"x":334,"y":576,"width":716,"height":717,"rx":352,"fill":339,"stroke":340,"strokeWidth":341},"220","100",[323,719,722],{"x":720,"y":721,"textAnchor":327,"fontSize":338,"fontWeight":329,"fill":330},"136","98","CI fails",[323,724,726],{"x":720,"y":725,"textAnchor":327,"fontSize":352,"fill":330},"122","header prints",[323,728,730],{"x":720,"y":729,"textAnchor":327,"fontSize":352,"fill":365},"142","seed 2718281828",[74,732],{"x1":733,"y1":734,"x2":735,"y2":734,"stroke":330,"strokeWidth":736,"markerEnd":737},"250","120","286","1.6","url(#rp-a)",[315,739],{"x":740,"y":576,"width":716,"height":717,"rx":352,"fill":741,"stroke":594,"strokeWidth":341},"292","#f7f0da",[323,743,745],{"x":744,"y":721,"textAnchor":327,"fontSize":338,"fontWeight":329,"fill":330},"402","reproduce locally",[323,747,748],{"x":744,"y":725,"textAnchor":327,"fontSize":352,"fill":330},"TEST_SEED=… -k test",[323,750,751],{"x":744,"y":729,"textAnchor":327,"fontSize":352,"fill":609},"identical values",[74,753],{"x1":754,"y1":734,"x2":622,"y2":734,"stroke":330,"strokeWidth":736,"markerEnd":737},"516",[315,756],{"x":757,"y":576,"width":758,"height":717,"rx":352,"fill":374,"stroke":375,"strokeWidth":341},"558","216",[323,760,762],{"x":761,"y":721,"textAnchor":327,"fontSize":338,"fontWeight":329,"fill":330},"666","fix + pin",[323,764,765],{"x":761,"y":725,"textAnchor":327,"fontSize":352,"fill":330},"explicit test with",[323,767,768],{"x":761,"y":729,"textAnchor":327,"fontSize":352,"fill":392},"the bad input",[398,770,771],{},"The seed makes the failure reproducible today; the explicit case keeps it fixed after the seed has changed.",[22,773,775],{"id":774},"frequently-asked-questions","Frequently Asked Questions",[10,777,778,781,782,784,785,787],{},[447,779,780],{},"Is calling random.seed() at the top of a test enough?","\nIt works for code using the module-level functions, but it is global state: any other code consuming random numbers in between shifts the sequence, and parallel workers each have their own global generator. Passing an explicit ",[14,783,410],{}," or NumPy ",[14,786,428],{}," instance is more robust and makes the dependency visible.",[10,789,790,793,794,796,797,800],{},[447,791,792],{},"What does pytest-randomly do with seeds?","\nIt shuffles test order and reseeds ",[14,795,35],{},", NumPy's legacy global generator and Faker at the start of each test from a seed printed in the header. Re-running with ",[14,798,799],{},"-p randomly -p \"randomly_seed=N\""," reproduces both the order and the random values.",[10,802,803,806,807,809,810,812,813,816,817,819],{},[447,804,805],{},"Should NumPy code use np.random.seed?","\nNo. ",[14,808,432],{}," controls the legacy global ",[14,811,436],{},". New code should use ",[14,814,815],{},"np.random.default_rng(seed)",", which returns an independent ",[14,818,428],{}," with better statistical properties, and pass that generator to the functions that need randomness.",[22,821,823],{"id":822},"related","Related",[27,825,826,833,839,846],{},[30,827,828,832],{},[54,829,831],{"href":830},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests\u002F","Controlling Time and Randomness in Tests"," — the wider approach to non-deterministic inputs.",[30,834,835,838],{},[54,836,837],{"href":56},"Generating Reproducible Fake Data with Faker"," — the same per-test derivation for Faker.",[30,840,841,845],{},[54,842,844],{"href":843},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-tests-in-ci-and-containers\u002Fbisecting-test-order-dependencies\u002F","Bisecting Test-Order Dependencies"," — what shuffled order reveals.",[30,847,848,852],{},[54,849,851],{"href":850},"\u002Fproperty-based-fuzz-testing-strategies\u002Fdesigning-strategies-for-domain-data\u002Fgenerating-dataframes-and-arrays-with-hypothesis-extras\u002F","Generating DataFrames and Arrays with Hypothesis Extras"," — exploring random inputs rather than fixing them.",[10,854,855,856],{},"← Back to ",[54,857,831],{"href":830},[859,860,861],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":68,"searchDepth":81,"depth":81,"links":863},[864,865,866,867,868,869,870,871,872],{"id":24,"depth":81,"text":25},{"id":60,"depth":81,"text":61},{"id":403,"depth":81,"text":404},{"id":440,"depth":81,"text":441},{"id":513,"depth":81,"text":514},{"id":635,"depth":81,"text":636},{"id":652,"depth":81,"text":653},{"id":774,"depth":81,"text":775},{"id":822,"depth":81,"text":823},"Make randomised code reproducible in tests: injected Random instances, NumPy Generator seeding, per-test seeds from node ids, pytest-randomly, and printing seeds for replay.","md",{"slug":876,"type":877,"breadcrumb":878,"datePublished":879,"dateModified":879,"faq":880,"howto":887},"seeding-random-and-numpy-for-reproducible-tests","article","Seeding Randomness","2026-09-18",[881,883,885],{"q":780,"a":882},"It works for code using the module-level functions, but it is global state: any other code consuming random numbers in between shifts the sequence, and parallel workers each have their own global generator. Passing an explicit random.Random or numpy Generator instance is more robust and makes the dependency visible.",{"q":792,"a":884},"It shuffles test order and reseeds random, NumPy's legacy global generator and Faker at the start of each test from a seed printed in the header. Re-running with -p randomly -p 'randomly_seed=N' reproduces both the order and the random values.",{"q":805,"a":886},"No. np.random.seed controls the legacy global RandomState. New code should use np.random.default_rng(seed), which returns an independent Generator with better statistical properties, and pass that generator to the functions that need randomness.",{"name":888,"description":889,"steps":890},"How to make randomised code reproducible in tests","Inject generator instances, derive a stable seed per test, print the session seed, and never rely on global random state.",[891,894,897,900,903],{"name":892,"text":893},"Inject the generator","Give randomised functions a random.Random or numpy Generator parameter instead of calling module-level functions.",{"name":895,"text":896},"Derive a seed per test","Hash a session seed with the test's node id so each test's sequence is stable regardless of order.",{"name":898,"text":899},"Print the session seed","Show it in the test header so any failing run can be replayed.",{"name":901,"text":902},"Use default_rng for NumPy","Create numpy Generators with np.random.default_rng(seed) rather than the legacy global state.",{"name":904,"text":905},"Assert on properties, not exact draws","Where possible assert invariants of random output, so tests survive a library changing its algorithm.","\u002Fadvanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests\u002Fseeding-random-and-numpy-for-reproducible-tests",{"title":5,"description":873},"advanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests\u002Fseeding-random-and-numpy-for-reproducible-tests\u002Findex","qMsM3IB27K3g5WcX1IBpZDahApRpfOD2l6UNyUoLN-I",1789718768887]