Skip to content

NumPy Random Numbers and Data Types

This lesson has two parts. First, random numbers: NumPy can make millions of them for test data, simulations, games and sampling. Second, data types (dtype): every NumPy array stores one type of value, and choosing and converting types correctly avoids nasty surprises.

How to run the examples

Create a new file named random_and_dtypes.py in your NumpyTutorials folder (see the setup in lesson 01). Any name works; this one just matches the topic. Start the file with these lines:

import numpy as np

Then copy the examples of this page into the same file, one after another, in the order shown, and run it with python random_and_dtypes.py (with your virtual environment active). Some examples use variables created in earlier ones, so keep everything in the one file. Each Output box shows what that example prints.

Part A: Random numbers

1. A random generator

Modern NumPy code first creates a random generator with np.random.default_rng(), then asks it for numbers. The number you pass in is the seed: with the same seed you get the same "random" numbers every time, which is perfect for tutorials and tests.

rng = np.random.default_rng(42)          # 42 = seed: same numbers on every run

print("dice rolls       :", rng.integers(1, 7, size=10))        # 1..6 (7 not included)
print("2x3 integers     :\n", rng.integers(0, 100, size=(2, 3)))
print("floats 0 to 1    :", rng.random(4))
print("uniform 10 to 20 :", rng.uniform(10, 20, size=4))
Output
dice rolls       : [1 5 4 3 3 6 1 5 2 1]
2x3 integers     :
 [[52 97 73]
 [76 71 78]]
floats 0 to 1    : [0.12811363 0.45038594 0.37079802 0.92676499]
uniform 10 to 20 : [16.4386512  18.22761613 14.43414199 12.27238722]
MethodGives
rng.integers(low, high, size)whole numbers from low up to but not including high
rng.random(size)floats from 0 up to 1
rng.uniform(low, high, size)floats between low and high, all equally likely

size can be a number (1D) or a tuple like (2, 3) for a 2D array. Because the seed is 42, you will get exactly these numbers when you run the file.

2. Normal distribution (bell curve)

Many real things, like people's heights, follow a bell curve: most values are near the average, and few are far away. rng.normal(loc, scale, size) creates such values: loc is the mean and scale is the standard deviation.

heights = rng.normal(loc=165, scale=8, size=10_000)    # mean 165 cm, std 8 cm
print("first 5 heights :", heights[:5].round(1))
print("mean            :", heights.mean().round(2))
print("std             :", heights.std().round(2))
print("between 157-173 :", ((heights > 157) & (heights < 173)).mean().round(3), "(about 68%)")
Output
first 5 heights : [168.  157.3 172.  164.6 163.5]
mean            : 164.92
std             : 8.05
between 157-173 : 0.678 (about 68%)
  • The mean and standard deviation of 10,000 random heights are very close to the 165 and 8 we asked for.
  • About 68% of values in a normal distribution lie within one standard deviation of the mean (157 to 173 here). Our random data shows exactly that.
  • (condition).mean() is a handy trick: True counts as 1 and False as 0, so the mean is the fraction of True values.

3. choice, shuffle and permutation

students = np.array(["Asha", "Ravi", "Neha", "Kiran", "Meena"])

print("pick 1            :", rng.choice(students))
print("pick 3 (no repeat):", rng.choice(students, size=3, replace=False))
print("coin x 10 (70% H) :", rng.choice(["H", "T"], size=10, p=[0.7, 0.3]))

print("permutation (copy):", rng.permutation(students))
print("original unchanged:", students)

rng.shuffle(students)                        # shuffle changes the array itself
print("after shuffle     :", students)
Output
pick 1            : Kiran
pick 3 (no repeat): ['Neha' 'Ravi' 'Kiran']
coin x 10 (70% H) : ['H' 'T' 'H' 'H' 'T' 'T' 'T' 'H' 'H' 'H']
permutation (copy): ['Ravi' 'Asha' 'Kiran' 'Meena' 'Neha']
original unchanged: ['Asha' 'Ravi' 'Neha' 'Kiran' 'Meena']
after shuffle     : ['Asha' 'Ravi' 'Kiran' 'Neha' 'Meena']
  • choice(arr) picks one random element; size=3, replace=False picks 3 different ones (like drawing names from a box without putting them back).
  • p=[0.7, 0.3] gives each option a probability: an unfair coin that shows "H" 70% of the time.
  • permutation() returns a shuffled copy; the original stays the same.
  • shuffle() shuffles the array itself and returns nothing.

4. Seeds make results repeatable

first = np.random.default_rng(7).integers(0, 100, 5)
second = np.random.default_rng(7).integers(0, 100, 5)
other = np.random.default_rng(8).integers(0, 100, 5)
print("seed 7 :", first)
print("seed 7 :", second, "<- same as above")
print("seed 8 :", other, "<- different")

# Old style you will see in older code (still works):
#   np.random.seed(42); np.random.rand(3); np.random.randint(1, 7, 10)
# Use default_rng() in new code.
Output
seed 7 : [94 62 68 89 57]
seed 7 : [94 62 68 89 57] <- same as above
seed 8 : [71 32 23 98 17] <- different

The same seed always gives the same sequence; a different seed gives a different one. Use a fixed seed when you want results others can reproduce, and np.random.default_rng() without a seed when you want different numbers on every run.

In older code you will see np.random.seed(42), np.random.rand() and np.random.randint(). They still work, but the NumPy documentation recommends default_rng() for new code.

Part B: Data types and type conversion

5. Common dtypes

examples = [
    np.array([1, 2, 3]),
    np.array([1.5, 2.5]),
    np.array([True, False]),
    np.array(["pen", "notebook"]),
    np.array([1 + 2j]),
    np.array([1, 2], dtype=np.int8),
    np.array([1, 2], dtype=np.float32),
]
for arr in examples:
    print(f"{str(arr):<22} dtype={str(arr.dtype):<10} itemsize={arr.itemsize} bytes")

# "<U8" means: text (Unicode) of at most 8 characters
Output
[1 2 3]                dtype=int64      itemsize=8 bytes
[1.5 2.5]              dtype=float64    itemsize=8 bytes
[ True False]          dtype=bool       itemsize=1 bytes
['pen' 'notebook']     dtype=<U8        itemsize=32 bytes
[1.+2.j]               dtype=complex128 itemsize=16 bytes
[1 2]                  dtype=int8       itemsize=1 bytes
[1. 2.]                dtype=float32    itemsize=4 bytes
dtypeHoldsBytes
int8, int16, int32, int64whole numbers; the number is the bits used1, 2, 4, 8
uint8whole numbers 0 to 255 (used for images)1
float32, float64decimal numbers4, 8
boolTrue / False1
<U8text of at most 8 characters4 per character
complex128complex numbers16

Python ints become int64 and Python floats become float64 by default.

6. Smaller dtypes use less memory

ages = np.arange(1_000_000) % 100            # values 0..99
for dtype in (np.int64, np.int32, np.int16, np.int8):
    print(f"{np.dtype(dtype).name:<6}: {ages.astype(dtype).nbytes / 1_000_000:.0f} MB")

print("int8 range :", np.iinfo(np.int8).min, "to", np.iinfo(np.int8).max)
print("int32 range:", np.iinfo(np.int32).min, "to", np.iinfo(np.int32).max)
print("float32 precision about", np.finfo(np.float32).precision, "digits,",
      "float64 about", np.finfo(np.float64).precision, "digits")
Output
int64 : 8 MB
int32 : 4 MB
int16 : 2 MB
int8  : 1 MB
int8 range : -128 to 127
int32 range: -2147483648 to 2147483647
float32 precision about 6 digits, float64 about 15 digits

One million ages take 8 MB as int64 but only 1 MB as int8. Smaller types save memory, but they have a range: np.iinfo shows that int8 only holds -128 to 127. np.finfo shows the precision of floats: float32 keeps about 6 digits, float64 about 15.

7. astype: converting types

arr.astype(new_type) returns a new array with every element converted.

prices = np.array([99.99, 149.50, -3.7])
print("float -> int   :", prices.astype(int), "(decimals are CUT, not rounded)")
print("round then int :", prices.round().astype(int))

text_numbers = np.array(["10", "20", "30"])
print("str -> int     :", text_numbers.astype(int) + 1)
print("str -> float   :", np.array(["1.5", "2.25"]).astype(float))

print("int -> bool    :", np.array([0, 1, 2, -5]).astype(bool))      # 0 is False
print("bool -> int    :", np.array([True, False, True]).astype(int))
print("int -> str     :", np.array([1, 22, 333]).astype(str))

try:
    np.array(["10", "abc"]).astype(int)
except ValueError as error:
    print("Error          :", error)
Output
float -> int   : [ 99 149  -3] (decimals are CUT, not rounded)
round then int : [100 150  -4]
str -> int     : [11 21 31]
str -> float   : [1.5  2.25]
int -> bool    : [False  True  True  True]
bool -> int    : [1 0 1]
int -> str     : ['1' '22' '333']
Error          : invalid literal for int() with base 10: np.str_('abc')
  • Float to int cuts off the decimals, it does not round: 99.99 becomes 99 and -3.7 becomes -3. Round first with .round() if that is what you want.
  • Text that looks like a number can be converted to int or float, which is very common after reading files.
  • To bool: 0 becomes False, every other number becomes True.
  • Text that is not a number raises a ValueError.

8. Overflow: a value too big for its type

small = np.array([100, 120, 127], dtype=np.int8)
print("int8 + 10      :", small + 10, "<- 130 and 137 do not fit in int8 (max 127)!")
print("int16 + 10     :", small.astype(np.int16) + 10)
Output
int8 + 10      : [ 110 -126 -119] <- 130 and 137 do not fit in int8 (max 127)!
int16 + 10     : [110 130 137]

int8 stops at 127. 120 + 10 = 130 does not fit, so it wraps around to the negative side and becomes -126, with no error and no warning. This silent bug is common with images (uint8); lesson 17 shows it on a real photo. Convert to a bigger type first, as in the second line.

9. Type promotion: mixing types in maths

When two arrays of different types meet, NumPy picks a result type that can hold both:

ints = np.array([1, 2, 3])
floats = np.array([0.5, 0.5, 0.5])

print("int + float  ->", (ints + floats).dtype)
print("int / int    ->", (ints / ints).dtype, "(division always gives float)")
print("int // int   ->", (ints // ints).dtype)
print("int8 + int16 ->", (np.array([1], np.int8) + np.array([1], np.int16)).dtype)
print("result_type(int32, float32) ->", np.result_type(np.int32, np.float32))
Output
int + float  -> float64
int / int    -> float64 (division always gives float)
int // int   -> int64
int8 + int16 -> int16
result_type(int32, float32) -> float64
  • int with float gives float.
  • / always gives a float, even for 4 / 2; // keeps whole numbers.
  • int8 with int16 gives the bigger int16.
  • np.result_type() tells you the result type without doing the calculation. int32 with float32 gives float64, because float32 cannot hold every int32 exactly.

Exercises

Try these exercises on your own before moving on:

  1. Roll two dice 1000 times (rng.integers) and find how often the total is 7.
  2. Make 5 random marks between 35 and 100 with seed 1. Run twice: same marks?
  3. Pick a random "student of the day" from a list of names.
  4. Convert np.array(["3.7", "8.2"]) to float, then to int. What happens to .7?
  5. Make np.array([250], dtype=np.uint8) and add 10. Explain the result.

Summary

  • Create a generator with rng = np.random.default_rng(seed) and use integers, random, uniform, normal, choice.
  • The same seed gives the same numbers: good for tests and tutorials.
  • permutation returns a shuffled copy; shuffle changes the array itself.
  • Every array has one dtype; smaller types save memory but have a limited range.
  • astype() converts; float to int cuts the decimals off.
  • Integer overflow wraps around silently; mixing types promotes to a type that fits both.

Next: Lesson 04: Indexing and Slicing.