Programming Fundamentals
Random number generation is when a program produces a value that cannot be predicted in advance. The value can change from run to run, so neither the programmer nor the user knows what it will be until the program produces it.
Most values in a program are fixed by the code or typed in by the user, so the same input always leads to the same result. A random value breaks that pattern, which makes a program feel fresh and fair. If a library book was always placed on the same shelf, a reader would learn the answer after one visit; if the shelf is chosen at random, every visit is a genuine search.
Random values are generated inside the program itself, so they are unpredictable rather than just unknown. Two runs can even produce the same value by chance, because random does not mean it never repeats. To act on what the value means, such as choosing a different shelf, the program needs selection, which is covered separately.
| Run | Program with a fixed value | Program with a random value |
|---|---|---|
| 1st | Book on shelf 3 | Book on shelf 7 |
| 2nd | Book on shelf 3 | Book on shelf 2 |
| 3rd | Book on shelf 3 | Book on shelf 7 |
Use a random number wherever an outcome should be decided by chance rather than by a rule: shuffling a playlist so a different song comes first each time, handing out seat numbers for a school concert, picking which pupil answers the next question from the class list, choosing a different warm-up task each lesson, or giving each takeaway order a random order number. In every case a fixed value would give the same result every time, while a user-entered value would not be chance at all.
Python gets its random numbers from the random library. A program creates a random number and can then use that value, for example to display a seat number.
import random
# Each run picks a different seat number for the concert
seat = random.randint(1, 120)
print("Your seat number is " + str(seat))Output:
Your seat number is 100
Each time this program runs, a new seat number is chosen, though by chance it could match an earlier one. That is exactly what you want for concert seating: nobody can work out in advance which seat they will be given.
Python cannot make a random number on its own, so it comes with a standard library called random. A library is a collection of ready-made functions, such as random.randint() and random.random(), that you can call whenever a program needs a value it cannot predict in advance.
A library is not switched on automatically. Built-in tools such as print() are always available, but anything stored in a library has to be made available to the program first by writing import random. This is the import statement, and it belongs at the top of the program, above every line that uses the library. Importing once is enough for the whole program, however many times you call random.randint() or random.random() afterwards.
# the library is made available once, at the top
import random
locker = random.randint(1, 60)
print("Your locker number is " + str(locker))Output:
Your locker number is 50The import comes first, so Python knows about the library before the call is reached and the program runs normally. Leave the import out, or put it below the line that uses the library, and Python has no idea what random means. It stops with a NameError, telling you the name is not defined, and nothing is printed.
# the import line has been left out
ticket = random.randint(1, 500)
print(ticket)Output:
NameError: name 'random' is not defined (line 2)So the pattern is always the same: ask for the library at the top, then use its functions freely below.
random.randint(low, high) returns a random whole number from a stated low value up to a stated high value. You can use it anywhere a program needs an unpredictable whole number, such as picking a shelf number, a seat number or a page to open.
The range of randint is inclusive at both ends. That means the low value and the high value are both possible results, as well as every whole number in between. Calling random.randint(3, 9) can give 3, 4, 5, 6, 7, 8 or 9, so there are seven possible answers.
A common slip is to assume randint stops one short of the high value, as range() does. It doesn't: the high value itself can be returned.
range() counts from its start value but stops before it reaches the end value, so the end value is never produced. randint behaves differently, and the table shows the effect of the same two numbers.
| Call | Smallest value | Largest value | End value included? |
|---|---|---|---|
random.randint(3, 9) | 3 | 9 | Yes |
range(3, 9) | 3 | 8 | No |
So the numbers you would type into range() are not the numbers to use in randint for the same set of values. range(1, 9) counts 1 to 8, while random.randint(1, 9) can also return 9.
A school library labels its shelves 3 to 9. To send a pupil to a random shelf, the low value is 3 and the high value is 9. Because both ends count, shelf 3 and shelf 9 are each just as possible as shelf 6, so no shelf is left out.
import random
shelf = random.randint(3, 9)
print("Collect your book from shelf", shelf)Output:
Collect your book from shelf 9random.random() takes no arguments — the brackets stay empty — and returns a random real number, one that can have a decimal part. Every value it produces is at least 0.0 and less than 1.0: 0.0 itself is possible, but exactly 1.0 never is.
import random
print("First decimal:", random.random())
print("Second decimal:", random.random())Output:
First decimal: 0.9664535356921388
Second decimal: 0.4407325991753527
Each call produces a new value, so the two lines will almost certainly show different decimals. On its own, a number like 0.4731 is not much use, so it usually gets scaled.
Suppose you want a real number between 2.0 and 6.0 instead. To scale the value, multiply it by the width of your target range, then add the lower bound. Multiplying stretches the 0-to-1 span so it is as wide as your range, and adding the lower bound slides that span up to start at the right place.
The pattern is low + random.random() * (high - low).
import random
low = 2.0
high = 6.0
rainfall = low + random.random() * (high - low)
print("Rainfall (mm):", rainfall)Output:
Rainfall (mm): 5.865814142768555
| Expression | Smallest possible | Largest possible |
|---|---|---|
| random.random() | 0.0 | just under 1.0 |
| low + random.random() * (high - low) | low | just under high |
Because 0.0 is possible, a scaled value can land exactly on the lower bound. Because 1.0 is not possible, it can creep very close to the upper bound but never reach it, so a rainfall of exactly 6.0 mm will never appear. That is different from random.randint, whose highest value is possible.
A stored random value is a random number that has been assigned to a variable so the program can use it again later. Each call to random.randint() or random.random() generates a new value rather than handing back an earlier one, so if you need the same number twice you must keep it in a variable.
A generated number is not kept anywhere on its own, so assign it straight away: opened = random.randint(1850, 2020). From then on opened holds that one number. It can be used in arithmetic, printed, or reused several times, and it stays the same unless you assign something new to the variable.
The two approaches behave very differently:
| Approach | What the program gets |
|---|---|
| Generate once into a variable, then use the variable | The same value every time it is used, so printed figures and totals always agree |
| Call the random function again each time it is needed | A new value on every call, so two parts of the output can disagree |
With a stored value, the opening year printed on screen and the centenary year worked out from it always match, because both come from one generated number.
import random
# one random value kept in a variable
opened = random.randint(1850, 2020)
print("The museum opened in " + str(opened) + ".")
print("Its centenary was in " + str(opened + 100) + ".")Output:
The museum opened in 1962.
Its centenary was in 2062.
Some programs ask a question built from a random number and then reveal the answer. Both parts must use the same number, so generate it once, store it, and use the variable in the question and in the answer. Calling the random function again for the answer would build the question around one number and give the answer for a different one, which makes the puzzle impossible to solve. In a full program the player's answer would be checked between the two lines, which needs selection.
import random
# one stored value, used in the clue and in the answer
number = random.randint(3, 9)
print("Clue: my number squared is " + str(number ** 2) + ".")
print("Answer: my number is " + str(number) + ".")Output:
Clue: my number squared is 81.
Answer: my number is 9.A random value is an ordinary number once it has been generated, so you can use it in arithmetic and in output in exactly the same way as a value you typed into the program yourself. You can add a constant (a fixed value that never changes) to it, multiply it by a constant, or add it to a second random value produced by a separate call.
A school art club sells raffle strips at £2 each. The number sold on Saturday is a random whole number from 12 to 25, produced by random.randint(12, 25). Multiplying that value by the constant 2 gives the money taken, and taking away the constant 18 that the club paid for the prize gives the profit. One run might sell 19 strips, giving £38 taken and £20 profit, while another run might sell 21 strips, giving £42 taken and £24 profit.
Two random values can be added together in the same way. A bakery sells a random number of white loaves, from 30 to 50, and a random number of brown loaves, from 20 to 40; adding the two values gives the total number of loaves sold on that run.
Printing a random value makes the output change from one run to the next, because the value is not known until the program runs. Store the generated value in a variable as soon as it is made, so the same number is used in every calculation and in the output; calling randint again gives you another value, so the printed figures might no longer match the calculation.
import random
saturday = random.randint(12, 25)
money = saturday * 2
profit = money - 18
print("Strips sold:", saturday)
print("Money taken:", money)
print("Profit:", profit)Output:
Strips sold: 24
Money taken: 48
Profit: 30
Run the program twice and you will see two different sets of printed numbers, even though nothing in the code changed.
A random integer can stand for one of several outcomes: you decide, before the program runs, what each possible value will mean. Python supplies only the number, so the meaning comes from your design rather than from the random library.
A robot vacuum cleaner picks its next movement with random.randint(1, 4), and its firmware fixes what each code means:
| Code | What the robot does |
|---|---|
| 1 | Drive forward one metre |
| 2 | Turn 90° left |
| 3 | Turn 90° right |
| 4 | Reverse one metre |
Four codes give four outcomes, so every movement is equally likely. The range must match the number of outcomes: randint(1, 6) would produce two codes the robot has no instruction for. randint includes both ends, so 1 to 4 really does contain four values.
A seating-plan program shows the same rule from the other direction. With eight tables it needs randint(1, 8); writing randint(1, 6) instead would leave tables 7 and 8 unused all year, because those numbers could never come up.
Each move needs its own code, so each call to random.randint() supplies one code. This program keeps both codes in separate variables, so each one can be looked up against the table above.
import random
# two moves in a row for the robot vacuum
move_one = random.randint(1, 4)
move_two = random.randint(1, 4)
print("First move code:", move_one)
print("Second move code:", move_two)Output:
First move code: 4
Second move code: 1Acting on the meaning — turning code 2 into an actual left turn inside the program — needs a decision. That is selection, covered elsewhere. The mapping is fixed in advance: the random call supplies the number, and the design supplies what it stands for.
When a program needs two random values, calling a random function twice gives two independent choices: the second call knows nothing about the first, so it takes its value from the full range all over again.
import random
delay1 = random.randint(1, 4)
delay2 = random.randint(1, 4)
print("First bus was late by", delay1, "minutes")
print("Second bus was late by", delay2, "minutes")Output:
First bus was late by 4 minutes
Second bus was late by 1 minutesBecause each call is independent, adding the two results does not spread the chance evenly across the totals. Some totals can be made in several different ways, so they come up more often than others. With each bus delay between 1 and 4 minutes, every pair is just as likely as any other, and there are sixteen pairs in total:
| Total delay | Pairs that make it | Number of pairs |
|---|---|---|
| 2 | 1 + 1 | 1 |
| 3 | 1 + 2, 2 + 1 | 2 |
| 4 | 1 + 3, 2 + 2, 3 + 1 | 3 |
| 5 | 1 + 4, 2 + 3, 3 + 2, 4 + 1 | 4 |
| 6 | 2 + 4, 3 + 3, 4 + 2 | 3 |
| 7 | 3 + 4, 4 + 3 | 2 |
| 8 | 4 + 4 | 1 |
So a total of 5 is four times as likely as a total of 2. Generating random.randint(2, 8) once would instead give every total exactly one chance in seven, a flat pattern with no peak in the middle.
Independence also means repeats. Two separate calls can return the same value, because the function keeps no memory of what it produced before and never avoids an earlier result. Often that is exactly what you want, such as two buses both being 3 minutes late.
Sometimes repeats are not allowed. A teacher giving four different project topics, numbered 1 to 10, to four students must not pick the same number twice, so the program has to check rather than assume:
Going round again until a new value appears needs iteration, and the comparison needs selection; those are separate ideas. What matters here is that a random function never guarantees unique results on its own, so a program that needs distinct values must compare each new one with those already stored and generate again when it is a repeat.