# How to Generate Random Numbers in Python (random, NumPy and Seeds)

You generate a random number in Python with the standard library's `random` module for a single value or a short sequence, or with NumPy when you need arrays of random numbers or statistical distributions. Both libraries are pseudorandom, meaning the numbers come from a deterministic algorithm, so passing a seed such as `42` makes the same sequence appear again on the next run.

That single distinction, deterministic algorithm versus true physical randomness, explains most of the confusion people hit when they write simulation code, build a train/test split, shuffle a card deck, or try to reproduce a machine learning result. This guide covers the functions you will actually call, how `random.randint` differs from NumPy's integer functions, what a seed does to the internal state, and the mistakes that quietly break reproducibility.

## What "Random" Means Inside a Python Program

A computer cannot flip a coin. It can only run arithmetic. A pseudorandom number generator (PRNG) is an algorithm that keeps an internal state, applies a mathematical transformation to that state, and returns the transformed value as the next "random" number. The output looks unpredictable because the transformation scrambles the state thoroughly, but it is fully determined by two things: the algorithm and the starting state.

Two terms matter here.

The **seed** is the initial value handed to the generator. It is what turns a fixed algorithm into a stream of numbers that looks different each time.

The **state** is the generator's entire internal memory at any moment. If you snapshot the state and restore it later, you get the identical continuation of the sequence.

Python's standard `random` module uses the Mersenne Twister algorithm, specifically MT19937, as its core generator. The CPython documentation describes it as producing 53-bit precision floats with a period of 2**19937 - 1. A period that large means the sequence repeats only after an astronomically long run, so repetition is not a practical concern for simulations or games.

NumPy takes a different path. Its modern API defaults to the PCG64 bit generator, which has a 128-bit period and passes stringent statistical test batteries. The older NumPy functions use a Mersenne Twister variant for backward compatibility.

Neither library produces cryptographic randomness. That point returns later, because using `random` to build passwords or session tokens is a genuine security bug rather than a style preference.

## The Python random Module: One Value at a Time

Import the module once at the top of your file.

```python
import random
```

### random.random() Returns a Float Between 0 and 1

`random.random()` returns a float in the half-open interval `[0.0, 1.0)`. The value can be exactly 0.0. It can never be exactly 1.0. That half-open behavior is deliberate and useful, because multiplying by a range length gives you a clean bucket:

```python
x = random.random() * 10   # 0.0 up to but never including 10.0
```

If you need a float across an arbitrary interval, `random.uniform(a, b)` returns `a + (b - a) * random()`. The documentation notes that the endpoint `b` may or may not be included depending on floating point rounding, so do not build logic that depends on hitting an endpoint exactly.

### Random Integers: random.randint and random.randrange

This is the part that trips people up most often. `random.randint(a, b)` returns an integer N with `a <= N <= b`. Both endpoints are inclusive.

```python
random.randint(1, 6)    # simulates a six-sided die, outcomes 1 through 6
random.randint(0, 9)    # ten possible outcomes: 0, 1, 2, ..., 9
```

`random.randint` is implemented as a call to `randrange(a, b + 1)`, and the documentation says so directly. That means `random.randint(0, 9)` and `random.randrange(0, 10)` are the same thing.

`random.randrange(start, stop, step)` follows the same convention as Python's `range` function. The `stop` value is excluded.

```python
random.randrange(10)          # 0 through 9
random.randrange(0, 10, 2)    # 0, 2, 4, 6, 8
```

For raw bit generation, `random.getrandbits(k)` returns a non-negative integer with `k` random bits. `random.getrandbits(8)` gives a value from 0 to 255.

### random.choice and the Sequence Functions

`random.choice(seq)` picks one element uniformly from a non-empty sequence. Pass a list to get a list element, pass a string to get one character, pass a tuple to get one tuple element.

```python
colors = ["red", "green", "blue"]
random.choice(colors)
```

An empty sequence raises `IndexError`. Guard against that, because an empty list is a common runtime condition when data loading fails.

Three related functions do more work.

`random.choices(population, weights=None, k=1)` returns a list of `k` elements drawn **with replacement**. Supply `weights` to bias the draw. This is the function you want for weighted sampling such as loot tables or categorical simulation.

```python
random.choices(["common", "rare", "epic"], weights=[70, 25, 5], k=3)
```

`random.sample(population, k)` returns a list of `k` distinct elements drawn **without replacement**. If `k` exceeds the population length, it raises `ValueError`. Since Python 3.11 the population must be a sequence, so sets are no longer accepted.

```python
random.sample(range(1, 50), 6)   # six distinct lottery-style numbers
```

`random.shuffle(x)` shuffles a mutable sequence in place and returns `None`. This is the single most common source of the `NoneType object is not subscriptable` error, because people write `deck = random.shuffle(deck)` and destroy their own list reference. Write it as two lines.

```python
random.shuffle(deck)
```

### Continuous and Named Distributions

Beyond `uniform`, the module ships `gauss(mu, sigma)` for a [normal distribution](/blog/data-analysis/normal-distribution-definition-examples), `normalvariate(mu, sigma)` as a slower but independent implementation, `expovariate(lambd)` for exponential waiting times, `triangular`, `betavariate`, `gammavariate` and `lognormvariate`. For most work `gauss` is fine. The documentation notes that `gauss` is slightly faster than `normalvariate`.

## Seeding and Reproducibility

`random.seed(a=None, version=2)` initializes the generator. With no argument, or `None`, Python pulls entropy from the operating system, which is why your script produces different numbers each run.

Pass an integer to make the sequence reproducible.

```python
random.seed(42)
print(random.random())   # prints the same value on every run, every machine
```

The `a` parameter accepts an int, str, bytes or bytearray. With `version=2`, the default, a str or bytes seed is converted to an integer using all of its bits with SHA-512. That means `random.seed("experiment-1")` works and gives a stable, repeatable stream.

Two properties of seeding deserve attention.

First, seeding happens once. Calling `random.seed(42)` inside a loop restarts the stream every iteration, so you get the same value over and over. This is the classic bug in simulation code. Seed at the top of the program or at the top of the function that owns the randomness, never inside the loop that consumes it.

Second, the module-level functions share one global generator. If two threads, two functions, or two imported libraries all call `random.random()`, they interleave on the same state. The fix is an instance.

```python
rng = random.Random(42)
rng.random()
```

A `random.Random` object has its own state and is unaffected by anything else in the process. Use one instance per component that needs its own independent, reproducible stream.

For checkpointing, `random.getstate()` returns a tuple capturing the current state and `random.setstate(state)` restores it. This lets you pause a long simulation, save progress, and resume the exact same sequence later.

## NumPy Random: Arrays, Speed and Two APIs

NumPy exists to do arithmetic on whole arrays at once. That is exactly what most simulations, bootstraps and machine learning pipelines need. Generating one million floats with `random.random` in a Python loop takes roughly three orders of magnitude longer than a single NumPy call of the same size.

NumPy currently carries two APIs. Knowing which one you are reading matters, because they disagree on a critical detail.

### The Legacy API: np.random.seed and Friends

```python
import numpy as np
np.random.seed(42)
np.random.rand(3)              # array of 3 floats in [0, 1)
np.random.randint(0, 10, 5)    # 5 integers from 0 up to but not including 10
np.random.randn(1000)          # 1000 draws from a standard normal
```

The legacy functions are module-level and share a global state, mirroring the design of Python's `random` module. They remain in NumPy for backward compatibility, and the NumPy documentation recommends them only for maintaining old code.

The trap is here: `np.random.randint(low, high)` treats `high` as **exclusive**, while `random.randint(a, b)` treats `b` as **inclusive**. `np.random.randint(0, 1)` always returns 0. The equivalent of a fair coin flip is `np.random.randint(0, 2)`.

`np.random.seed()` accepts only `None` or an integer. Passing a string raises `TypeError`, unlike Python's `random.seed`.

### The Modern API: np.random.default_rng

```python
rng = np.random.default_rng(42)
rng.random(3)                  # floats in [0, 1)
rng.integers(0, 10, size=5)    # integers, high exclusive by default
rng.integers(1, 6, endpoint=True)   # 1 through 6, inclusive of both ends
rng.normal(0, 1, size=1000)    # mean 0, standard deviation 1
rng.choice(["a", "b", "c"], size=4, p=[0.5, 0.3, 0.2])
```

`default_rng` returns a `Generator` object backed by PCG64. It is an instance, not a global, so it eliminates cross-module state collisions.

`rng.integers` returns integers in `[low, high)` by default. Set `endpoint=True` to include `high`, which finally gives NumPy a direct equivalent of `random.randint`. If `high` is omitted, the range becomes `[0, low)`.

The `size` argument accepts an integer or a shape tuple, so `rng.random((1000, 3))` returns a 1000 by 3 matrix in one call. The `dtype` argument lets you control the integer width, for example `dtype=np.uint8` for byte-range values.

`default_rng` also accepts an array of integers as a seed, a `SeedSequence`, or an existing `BitGenerator` or `Generator`. It does not accept strings.

### Independent Streams with SeedSequence

Parallel workloads need multiple reproducible streams that are not correlated. `np.random.SeedSequence` handles this without guesswork.

```python
ss = np.random.SeedSequence(12345)
children = ss.spawn(4)
workers = [np.random.default_rng(child) for child in children]
```

Each child seed produces a statistically independent stream. This is the correct replacement for the old pattern of seeding with `worker_id` or `base_seed + i`, which can produce correlated streams in some generators.

## Side-by-Side Comparison

| Task | `random` module | NumPy legacy | NumPy `Generator` |
|--|--|--|--|
| Float in `[0, 1)` | `random.random()` | `np.random.rand()` | `rng.random()` |
| Integer, both ends inclusive | `random.randint(a, b)` | not available directly | `rng.integers(a, b, endpoint=True)` |
| Integer, high exclusive | `random.randrange(a, b)` | `np.random.randint(a, b)` | `rng.integers(a, b)` |
| Pick one element | `random.choice(seq)` | `np.random.choice(arr)` | `rng.choice(arr)` |
| Pick k without replacement | `random.sample(seq, k)` | `np.random.choice(arr, k, replace=False)` | `rng.choice(arr, k, replace=False)` |
| Weighted pick | `random.choices(pop, weights, k)` | `np.random.choice(arr, k, p=...)` | `rng.choice(arr, k, p=...)` |
| Shuffle a sequence | `random.shuffle(lst)` | `np.random.shuffle(arr)` | `rng.shuffle(arr)` |
| Normal draws | `random.gauss(mu, sigma)` | `np.random.randn()` or `np.random.normal()` | `rng.normal(loc, scale, size)` |
| Set a seed | `random.seed(42)` | `np.random.seed(42)` | `np.random.default_rng(42)` |
| Recommended for new code | yes | no | yes |

## Reproducibility in Practice

Reproducibility means that the same seed and the same software produce the same numbers. It does not mean the numbers are random in a physical sense. It also does not mean every library call will match forever across software versions.

NumPy addresses this explicitly. Its published policy on versioned reproducibility, often cited as NEP 19, states that stream compatibility is guaranteed for the newer `Generator` methods given a fixed bit generator and the same version. The legacy module-level functions carry no such guarantee. The practical consequence is that a seed plus a NumPy version number plus a saved generator type is a complete reproduction recipe, while a bare `np.random.seed(42)` is not.

A second problem is library sprawl. Seeding NumPy does nothing for Python's `random` module, and seeding both does nothing for scikit-learn, TensorFlow, PyTorch or PySpark, which each have their own generators. A reproducible pipeline typically seeds every one of them and also sets `PYTHONHASHSEED` in the environment when set or dict ordering could affect results.

A third problem is thread count. Libraries that parallelize with reduction operations can produce different floating point sums depending on the number of threads, even with a fixed seed. For strict bit-level reproduction, pin the thread count as well.

Store the seeds you use. A seed is an experimental parameter. Log it next to the results the same way you log a parameter value, and if you tune a model, keep the evaluation seed fixed so that comparisons are fair.

## Randomness for Security Is a Different Problem

The `random` module's own documentation states plainly that its generators should not be used for security or cryptographic purposes. Mersenne Twister outputs are predictable once enough values are observed, which makes the stream recoverable by an attacker.

Python provides the `secrets` module for tokens, passwords, API keys and nonces.

```python
import secrets
secrets.randbelow(100)      # integer from 0 to 99
secrets.choice("abc123")    # one character
secrets.token_hex(16)       # 32-character hex string, 128 bits
secrets.token_urlsafe(32)   # URL-safe string for reset links
```

The `secrets` module draws from `os.urandom`, which the operating system fills from a cryptographically secure entropy source. There is no seed parameter. You cannot reproduce a token, and that is the point.

`random.SystemRandom` is the older route to the same entropy. `random.SystemRandom().randint(1, 6)` gives an unreproducible die roll backed by `os.urandom`. For new code, `secrets` is the clearer choice because the name states the intent.

## Practical Recipes

1. **Simulate a die roll.** `random.randint(1, 6)` gives one roll. For twenty rolls use `[random.randint(1, 6) for _ in range(20)]`.
2. **Draw a lottery ticket.** `random.sample(range(1, 70), 5)` gives five distinct numbers. Do not use `choices` with `k=5`, because it allows duplicates.
3. **Shuffle a deck or a dataset.** `random.shuffle(deck)` reorders a list in place. For NumPy arrays use `rng.permutation(len(arr))` and index with it.
4. **Bootstrap resample.** `rng.choice(data, size=len(data), replace=True)` produces one bootstrap sample. Repeat inside a loop for a confidence interval.
5. **Weighted category draw.** `random.choices(labels, weights=probs, k=1)[0]` returns one label. Weights do not need to sum to 1, since the function normalizes them.
6. **Train/test split by hand.** Shuffle an index list with `random.shuffle(indices)` and slice the first 80 percent. Seed the shuffle so the split is identical across experiments.
7. **Synthetic normal data.** `rng.normal(loc=170, scale=8, size=5000)` produces 5,000 draws for a height-like distribution with mean 170 and standard deviation 8.
8. **Quick uniformity check.** Generate 10,000 values with `rng.random(10000)` and plot a histogram. It should be flat. A mean near 0.5 and a standard deviation near 0.2887 confirms the expected first and second moments for a [uniform distribution](/blog/data-analysis/uniform-distribution) on `[0, 1)`.

## Common Mistakes and Limitations

**Confusing inclusive and exclusive upper bounds.** `random.randint(0, 1)` returns 0 or 1. `np.random.randint(0, 1)` always returns 0. `rng.integers(0, 1)` also always returns 0. Read the upper bound rule every time you switch libraries.

**Seeding inside a loop.** `for i in range(100): random.seed(42); x = random.random()` produces one hundred identical values. Seed once, before the loop.

**Reassigning the result of shuffle.** `random.shuffle()` returns `None`. The list is modified in place. The same is true of `np.random.shuffle` for a one-dimensional array, while `rng.shuffle` also works in place. Use `np.random.permutation` when you want a new array back.

**Assuming the global generator is safe across threads or modules.** The module-level functions share state. Use `random.Random(seed)` or `default_rng(seed)` instances when that matters.

**Passing a string seed to NumPy.** `np.random.seed("abc")` raises `TypeError`. `np.random.default_rng("abc")` also raises `TypeError`. Convert the string to an integer yourself, for example with `int.from_bytes(hashlib.sha256(s.encode()).digest()[:8], "big")`.

**Using `sample` when duplicates are allowed.** `random.sample` refuses to draw more elements than the population contains. If you need repeated draws from a small pool, `random.choices` is the correct function.

**Modulo bias.** `random.randrange(n)` is built to avoid it, but `random.getrandbits(32) % n` is not, because the number of possible outputs is not a multiple of `n`. Use `randrange`, `randint` or `secrets.randbelow` instead of writing the modulo yourself.

**Treating a seed as a substitute for randomness.** A seed makes a sequence repeat. If you clear it every run to get "more randomness" you lose reproducibility without gaining security. Pick one: reproducible with a recorded seed, or unpredictable with `secrets`.

**Expecting NumPy arrays from the random module.** `random.choices(pop, k=1000)` returns a Python list, not an array. It will be slow to process with NumPy operations. Convert with `np.array(...)` or generate directly with `rng.choice`.

**Overflowing small integer dtypes.** `rng.integers(0, 300, dtype=np.uint8)` raises an error because 300 exceeds the 0 to 255 range of that dtype. Match the dtype to the value range.

**Reproducibility across machines and versions.** A fixed seed does not guarantee bit-identical results across NumPy versions, floating point libraries, or thread counts. Treat version pinning as part of the reproducibility story.

**Statistical quality is not the same as unpredictability.** MT19937 and PCG64 pass strong [statistical tests](/blog/guides/statistical-tests-choosing-the-right-one-for-your-data) and are fine for simulation. Neither resists a determined adversary. Different requirements, different tools.

## Frequently Asked Questions

### How do I generate a random number in Python?

Use `random.randint(a, b)` for an integer between two inclusive bounds, `random.random()` for a float in `[0, 1)`, or `np.random.default_rng().random()` for an array. Choose based on whether you need one value or an array.

### What is the difference between random.randint and random.randrange?

`random.randint(a, b)` includes both endpoints. `random.randrange(a, b)` includes `a` and excludes `b`, matching the behavior of Python's `range`. `randint(a, b)` is internally `randrange(a, b + 1)`.

### Why does np.random.randint(0, 1) always return 0?

NumPy's integer functions treat the upper bound as exclusive, so the valid values are only those strictly below 1. Use `np.random.randint(0, 2)` or `rng.integers(0, 1, endpoint=True)` for a coin flip.

### How does random.seed work?

`random.seed(value)` sets the initial state of the generator, so the same seed produces the same sequence on every run. With no argument it draws fresh entropy from the operating system, which is why runs differ by default.

### Is Python's random module secure enough for passwords?

No. The CPython documentation states that the module is not suitable for security or cryptographic work. Use the `secrets` module, which reads from the operating system's cryptographic entropy source and cannot be seeded.

### Can I get the same random numbers in NumPy and in Python's random module?

No. The two libraries use different algorithms and consume seeds differently, so the same integer seed produces different sequences. Seed each library independently and record both seeds.

### What is the fastest way to generate a million random numbers?

Use NumPy. `np.random.default_rng(42).random(1_000_000)` returns a million floats in a single call, far faster than a Python loop calling `random.random()` a million times.

### Why does random.shuffle return None?

`random.shuffle` reorders the list in place and returns nothing. Assigning its result overwrites your list with `None`. Call it as a statement and keep the original variable.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "How do I generate a random number in Python?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Use random.randint(a, b) for an integer between two inclusive bounds, random.random() for a float in the interval from 0 up to but not including 1, or np.random.default_rng().random() for an array. Choose based on whether you need one value or an array."
      }
    },
    {
      "@type": "Question",
      "name": "What is the difference between random.randint and random.randrange?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "random.randint(a, b) includes both endpoints. random.randrange(a, b) includes a and excludes b, matching the behavior of Python's range function. randint(a, b) is internally randrange(a, b + 1)."
      }
    },
    {
      "@type": "Question",
      "name": "Why does np.random.randint(0, 1) always return 0?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "NumPy's integer functions treat the upper bound as exclusive, so the only valid value strictly below 1 is 0. Use np.random.randint(0, 2) or rng.integers(0, 1, endpoint=True) for a coin flip."
      }
    },
    {
      "@type": "Question",
      "name": "How does random.seed work?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "random.seed(value) sets the initial state of the generator, so the same seed produces the same sequence on every run. With no argument it draws fresh entropy from the operating system, which is why runs differ by default."
      }
    },
    {
      "@type": "Question",
      "name": "Is Python's random module secure enough for passwords?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "No. The CPython documentation states that the module is not suitable for security or cryptographic work. Use the secrets module, which reads from the operating system's cryptographic entropy source and cannot be seeded."
      }
    },
    {
      "@type": "Question",
      "name": "Can I get the same random numbers in NumPy and in Python's random module?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "No. The two libraries use different algorithms and consume seeds differently, so the same integer seed produces different sequences. Seed each library independently and record both seeds."
      }
    },
    {
      "@type": "Question",
      "name": "What is the fastest way to generate a million random numbers?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Use NumPy. np.random.default_rng(42).random(1000000) returns a million floats in a single call, far faster than a Python loop calling random.random() a million times."
      }
    },
    {
      "@type": "Question",
      "name": "Why does random.shuffle return None?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "random.shuffle reorders the list in place and returns nothing. Assigning its result overwrites your list with None. Call it as a statement and keep the original variable."
      }
    }
  ]
}
</script>

## References

1. [Python documentation: random, generate pseudo-random numbers](https://docs.python.org/3/library/random.html)
2. [Python documentation: secrets, generate secure random numbers](https://docs.python.org/3/library/secrets.html)
3. [NumPy documentation: Random Generator](https://numpy.org/doc/stable/reference/random/generator.html)
4. [NIST SP 800-90A Rev. 1: Recommendation for random number generation using deterministic random bit generators](https://csrc.nist.gov/pubs/sp/800/90/a/r1/final)
5. [Matsumoto M, Nishimura T (1998). Mersenne Twister: a 623-dimensionally equidistributed uniform pseudo-random number generator. ACM Trans Model Comput Simul 8:3-30.](https://doi.org/10.1145/272991.272995)

## Related Articles

- [Python Dictionaries: What They Are and How to Use Them](/blog/data-analysis/python-dictionaries-guide)
- [Python enumerate() Function: Syntax and Examples](/blog/data-analysis/python-enumerate-function)
- [NumPy arange: How to Create Arrays of Numbers in Python](/blog/data-analysis/numpy-arange)
- [Pandas in Python: What It Is and How to Use DataFrames](/blog/data-analysis/pandas-in-python-dataframes)
- [R vs Python: Differences and When to Use Each](/blog/data-analysis/r-vs-python-data-analysis)