South Plantation High School logo
South Plantation High School · Procedural Programming with Python

The math & random Modules, and Nested Loops

import · sqrt, pow, ceil, floor, fabs, pi · randint, random, choice, shuffle, sample, randrange · Loops inside loops · Lesson review

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
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

WordWhat it meansExample
moduleA file of ready-made Python code that someone already wrote and tested. You borrow it instead of writing it yourself.math, random
importThe keyword that loads a module into your program so you can use it.import math
functionA named tool that does one job. You call it by writing its name followed by parentheses.math.sqrt(16)
argumentThe value you put inside the parentheses. It is the input the function works on.the 16 in math.sqrt(16)
return valueThe answer the function hands back. You can print it or save it in a variable.4.0
nested loopA 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)

  1. It finds the name math. This only works because import math already ran.
  2. It follows the dot and looks inside math for a tool named sqrt.
  3. It calls sqrt, sending it the argument 16.
  4. sqrt works out the answer and returns 4.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

NameWhat it doesExampleResult
math.sqrt(x)square rootmath.sqrt(16)4.0
math.pow(x, y)x to the power of ymath.pow(2, 10)1024.0
math.ceil(x)round up to a whole numbermath.ceil(4.1)5
math.floor(x)round down to a whole numbermath.floor(4.9)4
math.fabs(x)absolute value (distance from 0)math.fabs(-7)7.0
math.pithe number pi (no parentheses!)math.pi3.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.

Number line for ceil and floor On a number line, 2.4 sits between 2 and 3. floor moves it left to 2 and ceil moves it right to 3. -2.4 sits between -3 and -2. floor moves it left to -3 and ceil moves it right to -2. -3 -2 -1 0 1 2 3 2.4 floor: 2 ceil: 3 -2.4 floor: -3 ceil: -2 floor always moves left. ceil always moves right.
xmath.floor(x)math.ceil(x)
2.423
2.923
5.055
-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()?

ToolRule2.42.6-2.4
math.floor(x)always down (to the left)22-3
math.ceil(x)always up (to the right)33-2
round(x)to the nearest whole number23-2
int(x)chops off the decimal part (toward 0)22-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.

Cleaner output: print("Circle area:", round(area, 2)) shows 28.27 instead of fifteen decimal places.

  • math functions work inside bigger expressions.
  • ** is the power operator and runs before *.
  • Python evaluates inside the parentheses first.

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

NameWhat it gives youExamplePossible results
random.randint(a, b)a whole number from a to b, including brandom.randint(1, 6)1, 2, 3, 4, 5, or 6
random.randrange(start, stop)a whole number like range(): stop is not includedrandom.randrange(1, 6)1, 2, 3, 4, or 5
random.random()a decimal from 0.0 up to (not including) 1.0random.random()like 0.4719...
random.choice(seq)one random item from a list or stringrandom.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 itemsrandom.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

  1. import random loads the random module once.
  2. sides = 6 stores how many faces the die has, so you can change it in one place.
  3. 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.
  4. result = random.randint(1, sides) runs again on every pass of the loop, so each roll gets a new number.
  5. 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.

moves = ["rock", "paper", "scissors"]
computer = random.choice(moves)

random.random()

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 placeNo
Gives backNonea new list of k items
Works on strings?No (TypeError)Yes
Repeats an item?NoNo, 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.
Nested loop order For hour 1, minute goes 0, 1, 2. Then for hour 2, minute goes 0, 1, 2 again. Six total prints. hour = 1 hour = 2 minute = 0 minute = 1 minute = 2 minute = 0 minute = 1 minute = 2 2 outer × 3 inner = 6 prints
1 : 0
1 : 1
1 : 2
2 : 0
2 : 1
2 : 2

Trace it like Python does

StephourminuteWhat happens
110Outer loop starts. Inner loop starts at 0. Prints 1 : 0
211Only the inner loop moves. Prints 1 : 1
312Inner loop's last value. Prints 1 : 2
420Inner loop is done, so the outer loop moves to 2. The inner loop starts over at 0.
521Prints 2 : 1
622Prints 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 outer for.
  • 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)

iinner loopstars printedrow
1range(1): j = 01*
2range(2): j = 0, 12**
3range(3): j = 0, 1, 23***
4range(4): j = 0, 1, 2, 34****

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:

  1. Start it at zero: total = 0
  2. Add to it inside a loop: total += random.randint(1, 6) (short for total = total + ...)
  3. 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

playerturnrolltotal
Ana(start)0
Ana044
Ana126
Ana2511 → prints
Ben(start)0 (reset)
Ben066
Ben117
Ben2310 → 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

MistakeWrong codeWhat Python saysFix
Forgot to importprint(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 dotfrom 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 nameimport MathModuleNotFoundError: No module named 'Math'import math
Misspelled functionmath.squareroot(9)AttributeError: module 'math' has no attribute 'squareroot'math.sqrt(9)
Square root of a negativemath.sqrt(-4)ValueError: math domain errorUse a number 0 or higher
Parentheses after pimath.pi()TypeError: 'float' object is not callablemath.pi
randint numbers backwardsrandom.randint(6, 1)ValueError: empty range in randrange(6, 2)random.randint(1, 6)
Choice from an empty listrandom.choice([])IndexError: Cannot choose from an empty sequenceMake sure the list has items
Saving shuffle's resultcards = random.shuffle(cards)No error. cards becomes Nonerandom.shuffle(cards) on its own line
Sample too bigrandom.sample([1, 2, 3], 5)ValueError: Sample larger than population or is negativek must be at most the number of items
print() at the wrong levelan empty print() indented inside the inner loopNo error. Every value prints on its own lineIndent 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 typeIn plain wordsWhere 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.

Summary

Cheat sheet

math

import math
math.sqrt(16)    # 4.0
math.pow(2, 3)   # 8.0
math.ceil(4.1)   # 5
math.floor(4.9)  # 4
math.fabs(-7)    # 7.0
math.pi          # 3.14159...

random

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.