NumPy random numbers come from a generator you create once and then ask for values:
import numpy as np
rng = np.random.default_rng()
rng.random() # a decimal between 0 and 1
rng.integers(0, 10) # a whole number, 0 to 9
The older np.random.rand() and np.random.randint() still work and appear in most tutorials. They differ in one respect that matters, covered further down.
Starting with the question most people arrive with: a random number in a given range. Output is from Python 3.12.5, NumPy 2.5.3.
Generate a random number between 0 and 1
rng.random() is the direct answer:
import numpy as np
rng = np.random.default_rng()
print("one random number between 0 and 1:")
print(" ", rng.random())
print()
print("five of them:")
print(" ", rng.random(5))
print()
print("the range is half-open. 0.0 can come up, 1.0 never does.")
Output:
one random number between 0 and 1:
0.2626879481907136
five of them:
[0.15949681 0.23139859 0.35870114 0.66405422 0.90211386]
the range is half-open. 0.0 can come up, 1.0 never does.
rng.random() returns a decimal in the range 0 to 1.The interval is half-open, written [0, 1). Zero is possible, one is not. That convention lets you scale the result into any range without the top value landing twice as often.
Passing a number returns that many values as an array, which is far quicker than calling it in a loop.
Generate a random number between 0 and 10, or any range
Whole numbers come from integers() and decimals from uniform():
import numpy as np
rng = np.random.default_rng()
print("a whole number between 0 and 10:")
print(" rng.integers(0, 10) ->", rng.integers(0, 10), " 10 itself is excluded")
print(" rng.integers(0, 10, endpoint=True) ->", rng.integers(0, 10, endpoint=True), " 10 can appear")
print()
print("a decimal between 5 and 10:")
print(" rng.uniform(5, 10) ->", round(rng.uniform(5, 10), 4))
print()
print("several at once, by passing a size:")
print(" rng.integers(1, 7, 10) ->", rng.integers(1, 7, 10), " ten dice rolls")
print(" rng.uniform(-1, 1, 5) ->", np.round(rng.uniform(-1, 1, 5), 3))
print()
print("scaling by hand gives the same thing as uniform:")
print(" 5 + rng.random(3) * 5 ->", np.round(5 + rng.random(3) * 5, 4))
Output:
a whole number between 0 and 10:
rng.integers(0, 10) -> 5 10 itself is excluded
rng.integers(0, 10, endpoint=True) -> 2 10 can appear
a decimal between 5 and 10:
rng.uniform(5, 10) -> 8.9223
several at once, by passing a size:
rng.integers(1, 7, 10) -> [2 3 4 2 2 3 3 6 2 5] ten dice rolls
rng.uniform(-1, 1, 5) -> [-0.256 0.315 0.268 0.814 0.533]
scaling by hand gives the same thing as uniform:
5 + rng.random(3) * 5 -> [7.7377 5.9373 8.6872]
integers() for whole numbers, uniform() for decimals.The upper bound is excluded by default, so rng.integers(0, 10) gives 0 to 9. Add endpoint=True when you want 10 included, which is what people usually mean by “between 0 and 10”.
| You want | Call |
|---|---|
| A decimal, 0 to 1 | rng.random() |
| A decimal, 5 to 10 | rng.uniform(5, 10) |
| A whole number, 0 to 9 | rng.integers(0, 10) |
| A whole number, 0 to 10 | rng.integers(0, 10, endpoint=True) |
| A dice roll | rng.integers(1, 7) |
| Ten of them | Add a size: rng.integers(1, 7, 10) |
Scaling rng.random() by hand gives exactly what uniform() gives, so use whichever reads better. For evenly spaced rather than random values, np.linspace is the tool.
default_rng() and the legacy np.random functions
Two APIs exist, and both are supported:
import numpy as np
# the current way: make a generator, then ask it for numbers
rng = np.random.default_rng()
print("rng ->", type(rng).__name__)
print("rng.random() ->", rng.random())
print("rng.integers(0, 100) ->", rng.integers(0, 100))
print("rng.normal(0, 1) ->", round(rng.normal(0, 1), 4))
print()
# the older way, still supported
print("the legacy functions still work:")
print("np.random.rand() ->", np.random.rand())
print("np.random.random() ->", np.random.random())
print("np.random.randint(0, 100) ->", np.random.randint(0, 100))
print()
print("Both give random numbers. The difference is where the state lives:")
print("a Generator holds its own, the legacy functions share one global.")
Output:
rng -> Generator
rng.random() -> 0.9754511878847149
rng.integers(0, 100) -> 30
rng.normal(0, 1) -> 0.2144
the legacy functions still work:
np.random.rand() -> 0.9169473039095801
np.random.random() -> 0.706797744272383
np.random.randint(0, 100) -> 77
Both give random numbers. The difference is where the state lives:
a Generator holds its own, the legacy functions share one global.
np.random.default_rng() is the newer one. You make a generator and call methods on it. The legacy functions are module-level and share a single global state behind the scenes.
New code should use default_rng(). The legacy functions are not going away, so existing code does not need rewriting, but the global state causes the problem shown in the next section.
Seeding for reproducible random numbers
A seed makes the sequence repeatable:
import numpy as np
first = np.random.default_rng(7).integers(0, 100, 5)
second = np.random.default_rng(7).integers(0, 100, 5)
print("default_rng(7) twice:")
print(" first ->", first)
print(" second ->", second)
print(" identical ->", np.array_equal(first, second))
print()
print("a seed makes the sequence repeatable, which is what you want")
print("for tests, tutorials and anything a colleague must reproduce.")
print()
print("without a seed you get something different every run:")
print(" ", np.random.default_rng().integers(0, 100, 5))
print(" ", np.random.default_rng().integers(0, 100, 5))
Output:
default_rng(7) twice:
first -> [94 62 68 89 57]
second -> [94 62 68 89 57]
identical -> True
a seed makes the sequence repeatable, which is what you want
for tests, tutorials and anything a colleague must reproduce.
without a seed you get something different every run:
[22 80 76 63 47]
[21 13 1 38 92]
Pass any integer. The numbers are still statistically random; they are just the same random numbers each run, which is what makes a tutorial or a test reproducible.
Leave the seed out for genuinely different values on every run. That is the right choice for simulations and for anything where repeatability would be a flaw.
Why the global seed causes trouble
This is the practical reason to prefer a generator:
import numpy as np
print("the legacy seed is GLOBAL, so anything can disturb it:")
np.random.seed(1)
first = np.random.rand()
np.random.seed(1)
np.random.rand() # imagine a library called this
second = np.random.rand()
print(" seeded, first value ->", first)
print(" seeded, then something else ran, next value ->", second)
print(" same? ->", first == second)
print()
print("a Generator has its own state, so nothing else can move it:")
mine = np.random.default_rng(1)
theirs = np.random.default_rng(99)
print(" mine.random() ->", mine.random())
theirs.random() # a different generator, doing its own thing
print(" mine.random() ->", mine.random(), " unaffected by theirs")
Output:
the legacy seed is GLOBAL, so anything can disturb it:
seeded, first value -> 0.417022004702574
seeded, then something else ran, next value -> 0.7203244934421581
same? -> False
a Generator has its own state, so nothing else can move it:
mine.random() -> 0.5118216247002567
mine.random() -> 0.9504636963259353 unaffected by theirs
np.random.seed() sets one shared state. Any library that draws a number afterwards shifts the sequence, so seeding no longer guarantees the values you expected.
A generator you created is yours. Another generator drawing numbers has no effect on it, which makes results reproducible even inside a large program.
Creating random arrays of a given shape
Both APIs make arrays, and this is where they differ most visibly:
import numpy as np
rng = np.random.default_rng(5)
print("a 2 by 3 array of decimals:")
print(rng.random((2, 3)))
print()
print("a 3 by 3 array of whole numbers:")
print(rng.integers(0, 10, (3, 3)))
print()
print("the legacy function wants separate arguments, not a tuple:")
print(" np.random.rand(2, 3) ->")
print(np.random.rand(2, 3))
try:
np.random.rand((2, 3))
except TypeError as err:
print(" np.random.rand((2, 3)) -> TypeError:", err)
print()
print("Generator methods take a shape tuple; legacy rand takes separate ints.")
print("That single difference causes a lot of confusion.")
Output:
a 2 by 3 array of decimals:
[[0.80500292 0.80794079 0.51532556]
[0.28580138 0.0539307 0.38336888]]
a 3 by 3 array of whole numbers:
[[5 4 1]
[0 0 0]
[1 9 1]]
the legacy function wants separate arguments, not a tuple:
np.random.rand(2, 3) ->
[[0.62526904 0.83310524 0.90376465]
[0.43337143 0.08989722 0.38859955]]
np.random.rand((2, 3)) -> TypeError: 'tuple' object cannot be interpreted as an integer
Generator methods take a shape tuple; legacy rand takes separate ints.
That single difference causes a lot of confusion.
rand() takes separate integers.rng.random((2, 3)) takes a tuple. np.random.rand(2, 3) takes the dimensions as separate arguments and raises TypeError if you pass a tuple.
Mixing the two up is a common first error. Once the array exists, its shape behaves like any other NumPy array.
Picking and shuffling with choice and shuffle
For sampling from data you already have:
import numpy as np
rng = np.random.default_rng(11)
names = np.array(["ann", "bob", "cat", "dan"])
print("pick one:")
print(" ", rng.choice(names))
print()
print("pick two, without picking the same one twice:")
print(" ", rng.choice(names, 2, replace=False))
print()
print("pick two, repeats allowed:")
print(" ", rng.choice(names, 2))
print()
print("weighted, so 'ann' comes up most:")
print(" ", rng.choice(names, 6, p=[0.7, 0.1, 0.1, 0.1]))
print()
deck = np.arange(8)
rng.shuffle(deck)
print("shuffle rearranges in place ->", deck)
print("permutation returns a new array ->", rng.permutation(8))
Output:
pick one:
ann
pick two, without picking the same one twice:
['dan' 'ann']
pick two, repeats allowed:
['cat' 'cat']
weighted, so 'ann' comes up most:
['ann' 'ann' 'dan' 'ann' 'ann' 'dan']
shuffle rearranges in place -> [5 2 7 6 3 0 4 1]
permutation returns a new array -> [6 5 4 7 2 1 0 3]
replace=False prevents the same item being picked twice, which is what you want for drawing names or splitting data. The p argument sets the odds and must add up to 1.
shuffle() rearranges an array in place and returns nothing. permutation() leaves the original alone and hands back a new array, which is usually the safer choice.
Random numbers from a distribution
Not everything should be uniformly random:
import numpy as np
rng = np.random.default_rng(2)
print("uniform: every value equally likely")
print(" ", np.round(rng.uniform(0, 10, 6), 2))
print()
print("normal: clustered around the mean")
print(" mean 0, sd 1 ->", np.round(rng.normal(0, 1, 6), 3))
print(" mean 100, sd 15 ->", np.round(rng.normal(100, 15, 6), 1))
print()
sample = rng.normal(100, 15, 100_000)
print("over 100,000 draws from normal(100, 15):")
print(" actual mean ->", round(sample.mean(), 2))
print(" actual sd ->", round(sample.std(), 2))
print()
print("the shape of the distribution is the point, not the individual numbers.")
Output:
uniform: every value equally likely
[2.62 2.98 8.14 0.92 6. 7.29]
normal: clustered around the mean
mean 0, sd 1 -> [-0.325 0.774 0.281 -0.554 0.978 -0.311]
mean 100, sd 15 -> [ 95.1 88.1 106.8 98.5 108.2 90.9]
over 100,000 draws from normal(100, 15):
actual mean -> 99.98
actual sd -> 14.99
the shape of the distribution is the point, not the individual numbers.
uniform() makes every value equally likely. normal(mean, sd) clusters values around the mean, which matches heights, measurement errors and most natural variation.
Over enough draws the sample mean and standard deviation converge on what you asked for, as the run above shows.
NumPy random quick reference
rng = np.random.default_rng()once, then call methods on it.rng.random()for a decimal between 0 and 1.rng.integers(low, high)for whole numbers, withhighexcluded.rng.uniform(low, high)for decimals in a range.default_rng(42)to make the sequence reproducible.rng.choice(items, n, replace=False)to sample without repeats.- Generator methods take a shape tuple; legacy
rand()takes separate integers.
Related NumPy guides:
- NumPy shape and array dimensions
- NumPy arrays in Python
- NumPy zeros
- NumPy linspace
- 2D arrays in Python
- The randint function in Python
Frequently asked questions
How do I generate a random number between 0 and 1 in NumPy?
Create a generator with rng = np.random.default_rng() and call rng.random(). The result is at least 0 and always below 1.
How do I get a random integer between 0 and 10?
rng.integers(0, 10) gives 0 to 9, since the top is excluded. Add endpoint=True to include 10.
What is the difference between np.random.rand and rng.random?
They both return decimals between 0 and 1. rng.random() belongs to a generator with its own state and takes a shape tuple; np.random.rand() uses global state and takes separate integers.
Should I use np.random.seed or default_rng?
default_rng(seed) for new code. np.random.seed() sets a global state that any other code can disturb, so seeding it does not reliably reproduce your numbers.
How do I make NumPy random numbers reproducible?
Pass a seed: np.random.default_rng(42). The same seed produces the same sequence on every run and on any machine.
How do I create a random array with a specific shape?
Pass the shape as a tuple: rng.random((2, 3)) or rng.integers(0, 10, (3, 3)). The legacy np.random.rand(2, 3) takes the dimensions separately.
How do I pick random items from a list without repeats?
rng.choice(items, n, replace=False). The generator API is described in the NumPy random Generator documentation.
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn