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))
| Method | Gives |
|---|---|
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%)")
- 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)
choice(arr)picks one random element;size=3, replace=Falsepicks 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.
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
| dtype | Holds | Bytes |
|---|---|---|
int8, int16, int32, int64 | whole numbers; the number is the bits used | 1, 2, 4, 8 |
uint8 | whole numbers 0 to 255 (used for images) | 1 |
float32, float64 | decimal numbers | 4, 8 |
bool | True / False | 1 |
<U8 | text of at most 8 characters | 4 per character |
complex128 | complex numbers | 16 |
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")
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)
- Float to int cuts off the decimals, it does not round:
99.99becomes99and-3.7becomes-3. Round first with.round()if that is what you want. - Text that looks like a number can be converted to
intorfloat, 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)
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))
- int with float gives float.
/always gives a float, even for4 / 2;//keeps whole numbers.int8withint16gives the biggerint16.np.result_type()tells you the result type without doing the calculation.int32withfloat32givesfloat64, becausefloat32cannot hold everyint32exactly.
Exercises
Try these exercises on your own before moving on:
- Roll two dice 1000 times (rng.integers) and find how often the total is 7.
- Make 5 random marks between 35 and 100 with seed 1. Run twice: same marks?
- Pick a random "student of the day" from a list of names.
- Convert np.array(["3.7", "8.2"]) to float, then to int. What happens to .7?
- 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 useintegers,random,uniform,normal,choice. - The same seed gives the same numbers: good for tests and tutorials.
permutationreturns a shuffled copy;shufflechanges 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.