Review the lesson slides, then complete the assignment at the bottom of this page. Your assignment work is saved in this browser on this computer. When you check a task, your name, period, and which tasks are complete are shared with your teacher (your code is not). When you finish, print it or save it as a PDF to turn in.
1 / 18→ next · ← back · n notes · s script · f full
Lesson Review
math, random & Nested Loops
import mathimport randomfor inside for
Borrowing tools Python already built, and putting loops inside loops.
By the end of this lesson you will be able to:
Use import to bring a module into your program.
Use math.sqrt, math.pow, math.ceil, math.floor, math.fabs, and math.pi.
Use random.randint, random.random, random.choice, random.shuffle, random.sample, and random.randrange.
Write a nested loop (a loop inside a loop) and predict how many times it runs.
Words you will hear today
Word
What it means
Example
module
A file of ready-made Python code that someone already wrote and tested. You borrow it instead of writing it yourself.
math, random
import
The keyword that loads a module into your program so you can use it.
import math
function
A named tool that does one job. You call it by writing its name followed by parentheses.
math.sqrt(16)
argument
The value you put inside the parentheses. It is the input the function works on.
the 16 in math.sqrt(16)
return value
The answer the function hands back. You can print it or save it in a variable.
4.0
nested loop
A loop written inside the body of another loop.
a for inside a for
How to use this page: Use Next and Back (or the arrow keys). In a TRY IT editor, click ▶ Run or Step. Programs that use random give different results every time you run them. That is the point!
A module is a file of ready-made code.
An argument goes into a function; a return value comes out.
A nested loop is a loop inside another loop.
Python comes with hundreds of tools that other programmers already wrote and tested. Today we use two toolboxes, called modules: math, for calculations, and random, for games and simulations. Then we will put a loop inside another loop, which lets us build tables, patterns, and grids.
Modules
Bringing in a module with import
A module is a file of code you can use in your program. Python includes many modules, but you have to import one before you can use it.
Way 1: import the whole module
import math
print(math.sqrt(16)) # module name, a dot, then the function
Way 2: import just the names you need
from math import sqrt, pi
print(sqrt(16)) # no "math." needed
print(pi)
Put your import lines at the top of the program.
The dot in math.sqrt means "the sqrt that lives inside math."
In this class, use Way 1 unless your teacher says otherwise. It is clear where each function comes from.
What Python does with math.sqrt(16)
It finds the name math. This only works because import math already ran.
It follows the dot and looks insidemath for a tool named sqrt.
It callssqrt, sending it the argument 16.
sqrt works out the answer and returns4.0. That value takes the place of math.sqrt(16), so print shows 4.0.
Why do we need to import at all?
Python has hundreds of modules. If it loaded all of them every time, every program would start slowly and use extra memory. So Python starts with only the basics, like print, len, and range, and you ask for anything extra with import.
Import once, use many times. One import math at the top is enough for the whole program. You do not import again inside a loop or before each use.
Uncheck the box to remove the import line, then run it.
Reading the error
Without the import, Python stops with NameError: name 'math' is not defined. A NameError means "you used a name I have never heard of." Python reads your program top to bottom, and nothing above that line created the name math.
Python 3.13 adds a hint: Did you forget to import 'math'? Always read the last line of an error first. It tells you the kind of error and usually points straight at the fix.
import math once at the top. Then use math.name.
The dot means "look inside this module."
Forgetting the import causes a NameError.
Think of a module as a toolbox on a shelf. import takes the toolbox down so you can use it. Then you name the toolbox and the tool, with a dot in between: math dot sqrt. Turn off the import in the editor and read the error. Python even suggests the fix.
The math Module
Six tools from math
Name
What it does
Example
Result
math.sqrt(x)
square root
math.sqrt(16)
4.0
math.pow(x, y)
x to the power of y
math.pow(2, 10)
1024.0
math.ceil(x)
round up to a whole number
math.ceil(4.1)
5
math.floor(x)
round down to a whole number
math.floor(4.9)
4
math.fabs(x)
absolute value (distance from 0)
math.fabs(-7)
7.0
math.pi
the number pi (no parentheses!)
math.pi
3.141592653589793
Notice the types:sqrt, pow, and fabs always give a float (a decimal like 4.0). ceil and floor give an int (a whole number like 5).
math.pi is a value, not a function. Write math.pi, not math.pi(). With parentheses Python shows TypeError: 'float' object is not callable.
A closer look at each tool
math.sqrt(x) answers "what number times itself gives x?" Since 4 × 4 = 16, math.sqrt(16) is 4.0. It returns a float even when the answer is a whole number. Most square roots are not whole: math.sqrt(2) is 1.4142135623730951.
math.pow(x, y) multiplies x by itself y times. math.pow(2, 10) is 2 × 2 × ... (ten 2s) = 1024.0. The ** operator does the same job: 2 ** 10 is 1024. The difference is the type: math.pow always gives a float, and ** with two ints gives an int.
math.ceil(x) and math.floor(x) turn a decimal into a whole number, but they never "round to nearest." ceil always goes up and floor always goes down, even for 4.1 or 4.9. A number that is already whole stays the same: math.ceil(5.0) is 5.
math.fabs(x) gives the distance from 0, so the answer is never negative. math.fabs(-7) and math.fabs(7) are both 7.0. Python also has a built-in abs() that needs no import: abs(-7) is 7 (it keeps an int as an int).
math.pi is stored to about 15 decimal places, which is far more than you will ever need for school geometry.
Save the answer for later
A function call is replaced by its return value, so you can store it in a variable and use it again, just like any number:
side = math.sqrt(49) # side holds 7.0
print(side * 4) # perimeter of the square: 28.0
Functions use parentheses: math.sqrt(16). pi does not: math.pi.
sqrt, pow, fabs give floats. ceil and floor give ints.
** and abs() are built in; math.pow and math.fabs always give floats.
These six cover most of what you need for geometry and rounding. Two details to remember: pi is a stored value, so no parentheses. And look at the results column: some functions give decimals and some give whole numbers.
The math Module
Try it: math explorer
Pick a function and type a number. Python works it out instantly.
Try sqrt with 2, 81, and -9. What happens with a negative number?
Try ceil and floor with 3.2, 3.8, and -3.2.
Try pow with 2 and 8, then 9 and 0.5. (A power of 0.5 is the same as a square root!)
Try fabs with -12.5 and 12.5.
math.sqrt(-9) gives ValueError: math domain error. "Domain error" means the input is outside what the function can handle. Real numbers have no square root of a negative number.
Why does a power of 0.5 give a square root?
Raising to the power 0.5 means "find the number that, multiplied by itself, gives x." That is exactly what a square root is. So math.pow(9, 0.5) and math.sqrt(9) both give 3.0. You will see this again in algebra as x1/2.
Why so many decimal places?
Most square roots go on forever without repeating. A float can only hold about 16 significant digits, so Python shows as many as it can store. To show fewer, wrap the answer in round(): round(math.sqrt(2), 2) is 1.41.
Good habit: say your prediction out loud (or write it down) before you click Run. When the result surprises you, that is the moment you actually learn something.
math.sqrt of a negative number is a ValueError.
math.pow(x, 0.5) equals math.sqrt(x).
Use round(value, 2) to show fewer decimal places.
Use the explorer to test your guesses before you check. A great habit is predicting the answer first, then running it. Try the negative numbers on purpose, because those are where students usually get surprised.
The math Module
ceil vs. floor
Picture a number line. ceil (ceiling) moves up to the next whole number. floor moves down.
x
math.floor(x)
math.ceil(x)
2.4
2
3
2.9
2
3
5.0
5
5
-2.4
-3
-2
Negative numbers trick people.math.floor(-2.4) is -3, because -3 is lower (to the left) on the number line.
Real use: 23 students need buses that hold 10 each. math.ceil(23 / 10) is 3 buses. You cannot rent 2.3 buses!
The opposite question: how many buses can be completely full? math.floor(23 / 10) is 2. Ask yourself, "Do I need room for the leftovers (ceil), or only the full groups (floor)?"
How is this different from round() and int()?
Tool
Rule
2.4
2.6
-2.4
math.floor(x)
always down (to the left)
2
2
-3
math.ceil(x)
always up (to the right)
3
3
-2
round(x)
to the nearest whole number
2
3
-2
int(x)
chops off the decimal part (toward 0)
2
2
-2
For positive numbers, int() and floor agree. For negative numbers they do not: int(-2.4) just drops the .4 and gives -2, while math.floor(-2.4) moves down to -3. Pick the tool whose rule matches the problem, not the one that "usually works."
floor: down (left). ceil: up (right).
Whole numbers stay the same.
round goes to nearest; int chops toward 0.
Ceiling is above you, floor is below you. That is all you need to remember. The negative numbers feel backwards at first, but look at the number line: -3 is to the left of -2.4, so it is the floor.
The math Module
Using math in a real program
This program finds the area of a circle and the long side of a right triangle.
Area of a circle: pi times the radius squared: math.pi * radius ** 2
Pythagorean theorem: c = math.sqrt(a ** 2 + b ** 2)
The ** operator also raises to a power. radius ** 2 and math.pow(radius, 2) give the same value, except that math.pow always gives a float.
Try: a = 3, b = 4 gives a hypotenuse of exactly 5.0. Try a = 5, b = 12, then a = 1, b = 1.
Order of operations still applies
In math.pi * radius ** 2, Python does the ** first, then the *, just like PEMDAS. Only the radius is squared, not pi times the radius. If you want something done first, add parentheses.
Step by step with radius = 3, a = 3, b = 4
area = math.pi * radius ** 2
# = math.pi * 9 radius ** 2 first
# = 28.274333882308138 then multiply by pi
c = math.sqrt(a ** 2 + b ** 2)
# = math.sqrt(9 + 16) square each side
# = math.sqrt(25) add inside the parentheses
# = 5.0 the sqrt runs last
Python always works from the inside out: everything inside the parentheses of math.sqrt( ) is finished first, and only then is the square root taken.
This is where the math module earns its keep. You can drop math dot pi and math dot sqrt right into a formula from your math class. Try a equals 1 and b equals 1 and see the square root of 2 printed with lots of decimal places.
The random Module
Six tools from random
Name
What it gives you
Example
Possible results
random.randint(a, b)
a whole number from a to b, including b
random.randint(1, 6)
1, 2, 3, 4, 5, or 6
random.randrange(start, stop)
a whole number like range(): stop is not included
random.randrange(1, 6)
1, 2, 3, 4, or 5
random.random()
a decimal from 0.0 up to (not including) 1.0
random.random()
like 0.4719...
random.choice(seq)
one random item from a list or string
random.choice(["rock", "paper", "scissors"])
"rock", "paper", or "scissors"
random.shuffle(list)
mixes up a list in place (returns None)
random.shuffle(cards)
cards is now in a new order
random.sample(seq, k)
a new list of k different items
random.sample(range(1, 50), 6)
like [7, 33, 2, 48, 19, 25]
Biggest difference to remember:randint(1, 6)can give 6. randrange(1, 6)cannot, because it follows the same "stop is not included" rule as range().
Sort them by what they work on
Make a number:randint, randrange, and random. You give a range (or nothing), and you get one number back.
Work with a list:choice, shuffle, and sample. You give a list (or string), and you get items from it.
Why have both randint and randrange?
randint reads like everyday speech: "a number from 1 to 6." randrange matches range(), which makes it handy for list positions. A list with 5 items has indexes 0 to 4, and random.randrange(5) gives exactly 0, 1, 2, 3, or 4. It can also take a step: random.randrange(0, 101, 10) picks from 0, 10, 20, ..., 100.
Every call is a fresh pick
Each time a line with random runs, Python makes a new pick. It does not remember earlier results and it does not try to "even things out," so rolling a 6 twice in a row is completely normal.
Need a decimal in a bigger range?
random.random() only goes from 0.0 to 1.0, but you can stretch it: random.random() * 10 gives a decimal from 0.0 up to (not including) 10.0.
randint includes both ends. randrange stops before stop.
randrange(len(items)) gives a valid index.
shuffle changes the list. sample makes a new list.
Every game you have ever played uses random numbers somewhere: dice, card shuffles, enemy moves, loot drops. Here are the six tools we will use. The one detail that trips people up is randint versus randrange. randint includes the last number; randrange does not.
The random Module
Try it: a dice roller
This program rolls a die several times using a loop and random.randint.
Run it a few times. The numbers change every time.
Change sides to 20 for a twenty-sided die.
The loop variable roll counts the rolls, and randint makes each result.
Is it really random? Computers use a formula that looks random, called a pseudo-random number generator. It is random enough for games and simulations.
Want the same "random" numbers every time? Programmers testing a game sometimes add random.seed(42) near the top. Starting the formula from the same seed gives the same sequence on every run, which makes bugs easier to repeat. Remove it when you want real variety again.
Line by line
import random loads the random module once.
sides = 6 stores how many faces the die has, so you can change it in one place.
for roll in range(1, 5 + 1): runs the body 5 times. The + 1 is there because range stops before its stop value, so roll goes 1, 2, 3, 4, 5.
result = random.randint(1, sides) runs again on every pass of the loop, so each roll gets a new number.
print("Roll", roll, "->", result) shows which roll it was and what came up.
Common mix-up: if you roll once above the loop and only print inside it, every line shows the same number. The randint call has to be inside the loop for each pass to get a new roll.
Each call to randint gives a new random number.
Put the random call inside the loop to get a new value each pass.
random.seed(n) repeats the same sequence (useful for testing).
Run this a few times and compare your results with your neighbor. You will get different numbers, because each call to randint picks a brand new one. Try a twenty-sided die and see how often you roll a 20.
The random Module
choice and random()
random.choice(seq)
Picks one item from a list or string. Each item has the same chance.
Gives a decimal from 0.0 up to, but not including, 1.0. It is handy for chances and percents:
if random.random() < 0.25: # about a 25% chance
print("Rare item found!")
Careful:random.choice([]) on an empty list shows IndexError: Cannot choose from an empty sequence.
choice works on strings too
A string is a sequence of characters, so random.choice("aeiou") picks one random vowel. It gives back one item, not a list.
Why does random() < 0.25 mean a 25% chance?
Picture a ruler from 0.0 to 1.0. random.random() lands anywhere on it with equal chance. The part from 0.0 to 0.25 is one quarter of the ruler, so about one call in four lands there. Change 0.25 to 0.10 for a 10% chance, or 0.5 for a coin flip.
Several outcomes from one number
Save the random number once, then check it with if / elif:
r = random.random()
if r < 0.60:
print("Common") # 60% of the time
elif r < 0.90:
print("Uncommon") # 0.60 to 0.90: 30%
else:
print("Rare") # the last 10%
Use r in every check. If you called random.random() again in the elif, you would be rolling a brand new number and the percentages would come out wrong.
choice picks one item from a sequence (a list or a string).
random() is between 0.0 and 1.0 (never exactly 1.0).
Save one random number, then compare it in an if/elif chain.
choice is perfect for picking a random word, color, or move. random with empty parentheses is perfect for chances: if random is less than 0.25, that happens about one time out of four. Run the editor a few times and watch both results change.
The random Module
shuffle vs. sample
shuffle(list)
sample(seq, k)
Changes the original?
Yes, mixes it in place
No
Gives back
None
a new list of k items
Works on strings?
No (TypeError)
Yes
Repeats an item?
No
No, all k are different
Classic bug:cards = random.shuffle(cards) sets cards to None, because shuffle gives back nothing. Just write random.shuffle(cards) on its own line.
What does "in place" mean?
"In place" means the function rearranges the very same list you gave it. There is no second copy. Since the original list already holds the new order, shuffle has nothing to hand back, so it returns None (Python's way of saying "no value").
Why can't you shuffle a string?
Strings cannot be changed after they are made, and shuffle works by changing things. sample does not change anything, so it works: random.sample("abc", 2) gives a list such as ['c', 'b'].
Want a mixed-up copy and keep the original?
Sample every item: mixed = random.sample(cards, len(cards)). Now mixed is shuffled and cards is unchanged.
About repeats: sample never picks the same position twice. If the list itself has duplicates, like ["red", "red", "blue"], you can still get "red" twice.
Step through this and watch cards in the Variables box. Notice that sample leaves it alone, and shuffle changes it.
shuffle changes the list and returns None.
sample returns a new list and leaves the original alone.
sample(items, len(items)) gives a shuffled copy.
Think of shuffle as shuffling a real deck of cards: the deck itself changes. Think of sample as drawing a few cards to look at, then putting the deck back the way it was. Step through the editor and watch the cards variable carefully at each line.
The random Module
Random playground
Pick a call and run it many times. The tally shows how often each result came up.
Compare randint(1, 6) with randrange(1, 6). Does a 6 ever show up for randrange?
Try randrange(0, 10, 2). Which numbers can it make?
Run sample(range(1, 50), 6). Do you ever see the same number twice in one result?
Fair results: after 100 runs of randint(1, 6), each number comes up roughly the same number of times, but almost never exactly the same. That is what randomness looks like.
More runs, smoother bars
With only 10 runs the bars can look very uneven: one number might show up 4 times and another not at all. With 100 or more runs the bars get closer to the same height. The more times you repeat a random experiment, the closer the results get to the "fair" share. This is why games and scientists test with many runs, not just one or two.
How randrange(0, 10, 2) works
Like range(0, 10, 2), it starts at 0, counts by 2, and stops before 10. The only possible values are 0, 2, 4, 6, and 8. It picks one of those each time.
Run a random call many times to see which results are possible.
More runs means the tally looks more even.
randrange(0, 10, 2) only makes even numbers 0 to 8.
Running a random function many times is how programmers test it. Run randrange of 1 comma 6 a hundred times and you will never see a 6. That is the clearest proof that stop is not included.
Nested Loops
A loop inside a loop
A nested loop is a loop inside the body of another loop.
for hour in range(1, 3): # outer loop
for minute in range(0, 3): # inner loop
print(hour, ":", minute)
The inner loop runs all the way through every time the outer loop runs once.
The inner loop is indented 4 more spaces, and its body 4 more again.
Total runs of the inner body = (outer count) × (inner count). Here: 2 × 3 = 6.
Think of a clock: the minute hand goes all the way around for every one move of the hour hand.
1 : 0
1 : 1
1 : 2
2 : 0
2 : 1
2 : 2
Trace it like Python does
Step
hour
minute
What happens
1
1
0
Outer loop starts. Inner loop starts at 0. Prints 1 : 0
2
1
1
Only the inner loop moves. Prints 1 : 1
3
1
2
Inner loop's last value. Prints 1 : 2
4
2
0
Inner loop is done, so the outer loop moves to 2. The inner loop starts over at 0.
5
2
1
Prints 2 : 1
6
2
2
Prints 2 : 2. Both loops are finished.
Notice: while the inner loop is running, hour stays exactly the same. The outer variable only changes after the inner loop has used up all its values.
Counting runs: the outer loop body runs 2 times. The inner loop body runs 3 times for each of those, so 2 × 3 = 6 times total. With 10 outer and 10 inner, the inner body runs 100 times. Nested loops can do a lot of work quickly!
Indentation is how Python knows what is nested. The inner for line is indented under the outer one, so it belongs to the outer loop's body. The print is indented again, so it belongs to the inner loop's body.
Inner loop finishes completely before the outer loop moves on.
The outer variable stays the same during the whole inner loop.
Total inner runs = outer × inner.
Here is the key idea: the inner loop starts over from the beginning every time the outer loop takes one step. For hour 1, minute goes 0, 1, 2. Then hour becomes 2, and minute starts over at 0. Two times three is six prints.
Nested Loops
Try it: step through a nested loop
Click Step and watch the Variables box. col runs through all its values before row moves on. The loop counters show the inner loop runs rows × cols times in total.
Prediction challenge: before you run it, figure out how many lines it will print with rows = 3 and cols = 4. Then check.
Three levels of indentation, three different jobs
print("row", row, "col", col) is indented 8 spaces, inside the inner loop. It runs rows × cols times.
print("--- end of row", row) is indented 4 spaces, inside the outer loop but after the inner loop. It runs once per row, so rows times.
print("Done!") has no indentation. It is outside both loops, so it runs once, at the very end.
Hint for the challenge: add up all three kinds of lines, not only the inner ones.
What to watch while you Step
The highlighted line jumps back up to the inner for after each inner print. That is the inner loop repeating.
After the last col, the highlight drops to the "end of row" line, then jumps back to the outerfor.
When row changes, col goes back to 1. The inner loop always starts fresh because its for line runs again from the top.
Watch col reset to 1 each time row changes.
Inner prints = rows × cols; the end-of-row print runs rows times; Done! runs once.
Step slowly here. Each time row changes, col starts over at 1. Look at the loop counters in the Variables box: the outer loop runs rows times, and the inner loop body runs rows times cols times in total.
Nested Loops
Building tables and patterns
Times table
The inner loop prints one row with end="\t" (a tab). The plain print() after the inner loop ends the row.
Star triangle
Here the inner loop's range depends on the outer variable: row 1 gets 1 star, row 2 gets 2 stars, and so on.
Where does print() go? Indented under the outer loop (4 spaces), it runs once per row. If you indent it 8 spaces, it moves inside the inner loop and every value lands on its own line.
What end= does
Normally print finishes by moving to a new line. The end= option replaces that new line with something else:
end="\t" puts a tab after the value, so numbers line up in columns.
end="" puts nothing after it, so the stars sit right next to each other.
An empty print() prints nothing but the new line, which ends the current row.
In the times table, the value printed is i * j: the row number times the column number. Row 3, column 4 shows 12.
Tracing the star triangle (rows = 4)
i
inner loop
stars printed
row
1
range(1): j = 0
1
*
2
range(2): j = 0, 1
2
**
3
range(3): j = 0, 1, 2
3
***
4
range(4): j = 0, 1, 2, 3
4
****
Because the inner loop uses range(i), it gets longer each row. Total stars = 1 + 2 + 3 + 4 = 10, not 4 × 4. The "multiply the counts" rule only works when the inner loop is the same size every time.
Challenge: how could you make the triangle upside down? (Hint: what if the inner range used rows - i + 1?)
Inner loop builds one row. print() after it starts a new row.
end="" and end="\t" keep print on the same line.
The inner range can use the outer loop variable; then add the counts instead of multiplying.
Nested loops are how we print anything with rows and columns: tables, grids, game boards, and patterns. Watch the empty print. Its indentation decides whether it runs after each row or after each single value.
Putting It Together
Loops + random + lists
Each player rolls a die 3 times, and their rolls are added up.
The outer loop goes through each player in a list.
total = 0 sits inside the outer loop, so each player starts fresh.
The inner loop rolls 3 times and adds each roll.
Try this bug on paper: what would happen if total = 0 were moved above the outer loop? (Ben's total would include Ana's rolls!)
The accumulator pattern
An accumulator is a variable that collects a running total. It always has three parts:
Start it at zero: total = 0
Add to it inside a loop: total += random.randint(1, 6) (short for total = total + ...)
Use it after the loop is done: print(player, ..., total)
Where you put each part decides what gets totaled. Start it inside the outer loop for one total per player. Start it above both loops for one grand total.
One possible run, traced
player
turn
roll
total
Ana
(start)
0
Ana
0
4
4
Ana
1
2
6
Ana
2
5
11 → prints
Ben
(start)
0 (reset)
Ben
0
6
6
Ben
1
1
7
Ben
2
3
10 → prints
Your rolls will be different, but the pattern is the same. Notice the print is indented 4 spaces, at the outer level, so it runs once per player after all 3 rolls. If it were indented 8 spaces you would see a running total after every single roll.
Reset an accumulator inside the outer loop when each outer item needs its own total.
Accumulator: start at 0, add inside the loop, use after the loop.
The print at the outer level shows one result per player.
This combines everything from today: a list, a random function, and a nested loop. Notice where total equals zero is. It is inside the outer loop, so every player starts at zero. Put it above the loop and the totals keep growing from player to player.
Debugging
Common mistakes
Mistake
Wrong code
What Python says
Fix
Forgot to import
print(math.sqrt(9))
NameError: name 'math' is not defined. Did you forget to import 'math'?
Add import math at the top
Used from, then the dot
from math import sqrt math.sqrt(9)
NameError: name 'math' is not defined. Did you forget to import 'math'?
Use sqrt(9), or import math
Capital letter in the module name
import Math
ModuleNotFoundError: No module named 'Math'
import math
Misspelled function
math.squareroot(9)
AttributeError: module 'math' has no attribute 'squareroot'
math.sqrt(9)
Square root of a negative
math.sqrt(-4)
ValueError: math domain error
Use a number 0 or higher
Parentheses after pi
math.pi()
TypeError: 'float' object is not callable
math.pi
randint numbers backwards
random.randint(6, 1)
ValueError: empty range in randrange(6, 2)
random.randint(1, 6)
Choice from an empty list
random.choice([])
IndexError: Cannot choose from an empty sequence
Make sure the list has items
Saving shuffle's result
cards = random.shuffle(cards)
No error.cards becomes None
random.shuffle(cards) on its own line
Sample too big
random.sample([1, 2, 3], 5)
ValueError: Sample larger than population or is negative
k must be at most the number of items
print() at the wrong level
an empty print() indented inside the inner loop
No error. Every value prints on its own line
Indent it to match the inner for line
Error messages shown are from Python 3.13. Older versions word some messages a little differently. Notice the odd message for randint(6, 1): randint uses randrange behind the scenes.
What each kind of error is telling you
Error type
In plain words
Where to look
NameError
"I don't know that name."
A missing import, or a typo in a variable or module name.
ModuleNotFoundError
"There is no module with that name."
The spelling or capitalization on the import line.
AttributeError
"That module exists, but it has no tool with that name."
The part after the dot. Check the spelling against the cheat sheet.
ValueError
"Right type of value, but this value won't work."
The numbers you passed in: a negative for sqrt, a backwards range, a sample that is too big.
TypeError
"You used this kind of thing the wrong way."
Extra parentheses (like math.pi()), or shuffling a string.
IndexError
"There's nothing at that position."
A list that is empty when you did not expect it to be.
A debugging routine: (1) Read the last line of the error for the type and message. (2) Find the line number it points to. (3) Check that line against the table above. For bugs with no error message (the shuffle bug, the print() bug), add a print() to show a variable's value, or use Step to watch the code run one line at a time.
NameError with "Did you forget to import" means add the import line.
The error type tells you what kind of problem to look for.
shuffle returns None, so never save its result.
Most module errors are about the import line or spelling. Python 3.13 is very helpful here: it literally asks if you forgot to import. The shuffle bug is sneaky because there is no error at all, just a variable that suddenly holds None.
Practice
Check yourself
Predict the answer, then click Check. This is practice only. Nothing is graded or recorded.
Ask yourself these three questions on every problem:
What type comes back?sqrt, pow, and fabs give a float like 4.0; ceil and floor give an int like 4.
Is the last number included?randint: yes. randrange and range: no.
How many times does the inner body run? Outer count × inner count, as long as the inner loop is the same size every time.
If you get one wrong, go back to the slide on that topic and try it in the editor before moving on.
Check the type: float or int?
For nested loops, multiply the counts.
Watch the details in these: is the answer a whole number or a decimal? Is the last number included? For the nested loop question, multiply instead of counting by hand.
import random
random.randint(1, 6) # 1 to 6
random.randrange(1, 6) # 1 to 5
random.random() # 0.0 to 1.0
random.choice(items) # one item
random.shuffle(items) # mixes items
random.sample(items, 2) # new list
Nested loops
for row in range(3):
for col in range(4):
print("*", end="")
print()
Inner loop runs fully for each outer step: 3 × 4 = 12 stars, in 3 rows of 4. end="" keeps the stars on one line, and the plain print() ends each row.
Before you run, check:
import math / import random is at the top, spelled in lowercase
You wrote module.function() with a dot, or used from ... import and then the plain name
math.pi has no parentheses
randint includes the last number; randrange does not
You did not save the result of shuffle
The inner loop and its body are indented one more level, and any row-ending print() lines up with the inner for
Any random call that should give a new value each time is inside the loop
Accumulators like total = 0 are reset at the right level (inside the outer loop for one total per item)
The big ideas in one sentence each
Modules: import a module once, then reach its tools with module.name.
math: it does calculations and rounding you would otherwise write yourself; watch for float vs. int results.
random: every call makes a fresh pick, so results change from run to run; test by running many times.
Nested loops: the inner loop runs all the way through for every single step of the outer loop, and indentation decides which loop each line belongs to.
import once, at the top.
Nested loop total = outer × inner.
Let's wrap up. Import a module once at the top, then use a dot to reach its tools. math gives you square roots, powers, rounding, absolute value, and pi. random gives you numbers, choices, and shuffles. And a nested loop runs its inner loop all the way through for every step of the outer loop.