Kestrel Exams kestrel logo KESTREL EXAMSPRACTICE TO PASS
Python11 min readCode verified

Modules: import, math, random, and writing your own

Python comes with thousands of ready-made functions, but most of them are packed away in modules until you ask for them. This guide covers import and the dot, the math and random modules a first course uses, writing a module of your own, and the three errors that come with all of it. Every listing was executed on CPython 3.11.

Practice this topic in the free app →All Python guides

A workshop of labelled drawers#

Picture a workshop wall covered in drawers, each with a label: MATH, RANDOM, TIME, TURTLE. Inside each drawer are tools. The tools exist the whole time, but you cannot use one until you open its drawer, and when you use it you say which drawer it came from, so nobody confuses the hammer from WOODWORK with the one from UPHOLSTERY.

A module is one of those drawers: a named collection of functions and values that you can load into your program. Usually it is a file of Python code; a few, such as math, are built into Python itself. import opens the drawer. The dot, as in math.sqrt, says which drawer the tool came from. A few tools such as print, len and round are always on the bench with no import needed; they are called built-in functions.

import and dot notation#

Write the import line once, at the top of the file. After that, use the module’s name, a dot, then the tool’s name:

import math

print(math.sqrt(16))
print(math.pi)
print(math.sqrt(2) * math.sqrt(2))
4.0
3.141592653589793
2.0000000000000004

math.sqrt(16) is dot notation: “the sqrt that lives in math”. Notice that math.sqrt returns 4.0, a float, even for a perfect square, and math.pi has no brackets because it is a value, not a function. The third line is a reminder that floats are stored in binary and are almost never exact: the square root of 2 is stored very slightly wrong, so squaring it gives 2.0000000000000004 rather than 2.0. Compare floats with a tolerance (or math.isclose), never with ==.

Forget the import and Python has never heard of math:

print(math.sqrt(16))
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    print(math.sqrt(16))
          ^^^^
NameError: name 'math' is not defined

There are two other ways to import, and each has a trade-off:

FormThen you writeTrade-off
import mathmath.sqrt(2)longest to type, clearest about where each name comes from
from math import sqrt, pisqrt(2)shorter, but only the names you listed become available in your file (the whole module is still loaded), and they can clash with your own names
import math as mm.sqrt(2)an alias: a shorter nickname for the module

You may also see from math import *, which dumps every name from the module into your program. Avoid it: you can no longer tell where a name came from, and one of them may quietly replace a variable of your own.

The math module#

import math

print(math.floor(3.7), math.ceil(3.2))
print(math.floor(-3.2), math.ceil(-3.7))
print(int(3.7), int(-3.7))
print(math.pow(2, 3), 2 ** 3)
print(math.fabs(-5), abs(-5))
print(round(2.5), round(3.5), round(2.675, 2))
3 4
-4 -3
3 -3
8.0 8
5.0 5
2 4 2.67

Three details from that output are worth knowing for an exam:

  • floor always rounds down and ceil always rounds up, and “down” for a negative number means further from zero: floor(-3.2) is -4. Both return an int.
  • int(-3.7) just chops off the decimals, giving -3. That is truncation, not rounding, and it differs from floor for negatives.
  • math.pow always returns a float, while the ** operator keeps whole numbers whole as long as the exponent is not negative (2 ** -1 is 0.5).

The built-in round (no import) has a surprise of its own: when a number is exactly half way, it rounds to the nearest even number. So round(2.5) is 2 and round(3.5) is 4. This is called banker’s rounding, and it is deliberate. The last value, round(2.675, 2) giving 2.67, has a different cause: 2.675 cannot be stored exactly in binary, and the stored value is a hair below it, so it rounds down.

The random module#

CallGives you
random.random()a float from 0.0 up to, but not including, 1.0
random.randint(a, b)a whole number from a to b, including both ends
random.randrange(a, b)a whole number from a up to b, not including b, like range
random.choice(seq)one item picked from a list or string
random.shuffle(lst)mixes up a list in place and returns None
random.seed(n)starts the random sequence at a fixed point, so the same numbers come out every run

The numbers are not truly random. They come from a formula that produces a sequence that looks random. Setting a seed picks where in that sequence to start, which is handy for testing: the program then behaves the same every time. That is how this listing can show its output at all:

import random

random.seed(42)
print(random.randint(1, 6), random.randint(1, 6), random.randint(1, 6))
print(round(random.random(), 4))
print(random.choice(["rock", "paper", "scissors"]))
deck = [1, 2, 3, 4, 5]
random.shuffle(deck)
print(deck)
6 1 1
0.7416
rock
[4, 1, 3, 5, 2]

Run it twice and you get exactly those numbers again. Remove the seed line and they change every run. Notice that shuffle changed deck itself; writing deck = random.shuffle(deck) would replace your list with None, the same trap as sort.

A common use is a simulation. Roll a die 6,000 times and count the results; each face should come up roughly 1,000 times:

import random

random.seed(7)
counts = [0, 0, 0, 0, 0, 0]
for roll in range(6000):
    face = random.randint(1, 6)
    counts[face - 1] = counts[face - 1] + 1
print(counts)
print(sum(counts))
[1038, 1020, 1000, 969, 953, 1020]
6000

Writing your own module#

Any .py file is a module. Put some functions in a file, save it in the same folder as your program, and import it by its file name without the .py:

The file geometry.py holds:

"""Small geometry helpers."""

PI = 3.14159

def circle_area(r):
    return PI * r * r

def rectangle_area(w, h):
    return w * h
import geometry

print(geometry.circle_area(2))
print(geometry.rectangle_area(3, 4))
print(geometry.PI)
12.56636
12
3.14159

The module’s functions and its constant PI are all reached with dot notation, exactly like math. Splitting a program into modules keeps each file short, lets several programs share the same functions, and is how every large Python program is organised.

if __name__ == "__main__"#

When Python imports a module, it runs the whole file once. So any test code you left at the bottom of the module runs too, inside the program that imported it:

The file tools.py holds:

def double(n):
    return n * 2

print("testing double:", double(21))
import tools

print("main program says", tools.double(5))
testing double: 42
main program says 10

The fix is a guard. Python sets a hidden variable called __name__ in every file: it is "__main__" in the file you ran directly, and the module’s own name in a file that was imported. So code under this if runs only when you run the module itself:

The file tools.py holds:

def double(n):
    return n * 2

if __name__ == "__main__":
    print("testing double:", double(21))
import tools

print("main program says", tools.double(5))
main program says 10

Run tools.py directly and the test lines run. Import it and they stay quiet. Many programs put their whole main code in a main() function and call it under this guard.

Three import errors and what they mean#

A misspelt module name fails at the import line:

import maths
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    import maths
ModuleNotFoundError: No module named 'maths'

A misspelt tool name imports fine and fails when used. The module exists; the thing inside it does not:

import math

print(math.squareroot(16))
Traceback (most recent call last):
  File "main.py", line 3, in <module>
    print(math.squareroot(16))
          ^^^^^^^^^^^^^^^
AttributeError: module 'math' has no attribute 'squareroot'

A file of your own with the same name as a library module is the sneaky one. Python looks in your program’s folder before the standard library, so a file called random.py sitting there is imported instead of the real random:

The file random.py holds:

print("This is my own random.py")
import random

print(random.randint(1, 6))
This is my own random.py
Traceback (most recent call last):
  File "main.py", line 3, in <module>
    print(random.randint(1, 6))
          ^^^^^^^^^^^^^^
AttributeError: module 'random' has no attribute 'randint'

The message says random has no randint, which sounds impossible, until you see the first line of output: the random that loaded was your own file. Never name your files after modules you use: random.py, turtle.py, csv.py. (A few modules, such as math, are built into Python and win anyway, but the habit is worth keeping for all of them.)

One sentence to carry into the exam

import loads a module once at the top; dot notation (math.sqrt) says which module a tool comes from. randint includes both ends, seed makes randomness repeatable, and code under if __name__ == "__main__": runs only when the file is run directly.

Where these go wrong#

  • Using a module without importing it. NameError: name 'math' is not defined. Add import math at the top.
  • Writing sqrt(16) after import math. With a plain import you must write math.sqrt. Only from math import sqrt lets you drop the prefix.
  • Treating randint(1, 6) like range(1, 6). randint includes 6; range and randrange stop before it.
  • deck = random.shuffle(deck). shuffle changes the list in place and returns None.
  • Expecting floor and int to agree on negatives. math.floor(-3.7) is -4; int(-3.7) is -3.
  • Expecting round(2.5) to be 3. Halves round to the nearest even number, so it is 2.
  • Naming your file after a module, such as random.py or turtle.py. Your file is imported instead of the real one.
  • Test code at the bottom of a module with no __name__ guard. It runs every time the module is imported.

Practice#

Answer each one on paper before you look at the solutions. Every answer that involves running code was checked by running it.

  1. What does each line print? import math, then print(math.sqrt(25)), print(math.floor(-2.5)), print(math.ceil(4.01)), print(int(-2.5)).
  2. Which of these can produce the value 10? (a) random.randint(1, 10) (b) random.randrange(1, 10) (c) random.choice(range(1, 11))
  3. Write a function hypotenuse(a, b) using from math import sqrt, and test it with 3 and 4.
  4. Simulate 1,000 coin flips with random.randint(0, 1) and count heads. Use a seed so the result is repeatable.
  5. A student’s dice program, dice.py, sits in a folder that also holds an old file they named random.py. dice.py runs import random then random.randint(1, 6) and crashes with AttributeError: module 'random' has no attribute 'randint'. Explain and fix.
  6. A module shapes.py ends with print(area(2, 3)) as a quick test. What happens when another program runs import shapes, and how do you stop it?

Solutions#

  1. 5.0 (sqrt returns a float), -3 (floor goes down, further from zero), 5 (ceil goes up), -2 (int truncates toward zero).
  2. (a) and (c). randint includes both ends. randrange(1, 10) stops before 10, like range. range(1, 11) includes 10, so choice can pick it.
  3. With from math import sqrt, the function is used without the math. prefix:
    from math import sqrt
    
    def hypotenuse(a, b):
        return sqrt(a ** 2 + b ** 2)
    
    print(hypotenuse(3, 4))
    print(round(hypotenuse(1, 1), 3))
    5.0
    1.414
  4. Count every flip that comes up 1. With seed 1 the result is the same every run; without it, it changes each time but stays close to 500:
    import random
    
    random.seed(1)
    heads = 0
    for flip in range(1000):
        if random.randint(0, 1) == 1:
            heads = heads + 1
    print("heads:", heads, "tails:", 1000 - heads)
    heads: 515 tails: 485
  5. Python finds the student’s own random.py in the program’s folder before the standard library, so import random imports that file, which has no randint. Rename or delete random.py. (If the program file itself is named random.py, the message says partially initialized module 'random' instead; the cause and the fix are the same.)
  6. Importing runs the whole file, so the test line prints inside the other program. Put it under if __name__ == "__main__":, which is true only when shapes.py itself is the file being run.

Test-readiness checklist#

These are the skills a first programming course tests on this topic. Tick each one only when you can do it without looking.

  • ☐  I can import a module and call its functions with dot notation.
  • ☐  I can explain import math, from math import sqrt and import math as m, and the trade-offs.
  • ☐  I can use sqrt, pi, floor, ceil and pow, and predict results for negative numbers.
  • ☐  I can use random, randint, randrange, choice, shuffle and seed, and say which include their end value.
  • ☐  I can write my own module, import it, and guard its test code with if __name__ == "__main__":.
  • ☐  I can read NameError, ModuleNotFoundError and AttributeError messages from imports and fix their causes.

Test yourself in the free Kestrel Exams app

12 practice questions on import, math, random, your own modules and __name__. Free, offline, no account.

Practise modules →

Frequently asked questions#

What is a module in Python?

A module is a Python file containing functions, values and other definitions that a program can load with import. Python comes with many standard modules, such as math and random, and any .py file you write whose name is a valid Python name (letters, digits and underscores, not starting with a digit) can be imported as a module by programs in the same folder.

What is the difference between import math and from math import sqrt?

import math loads the module and you write math.sqrt(16), which makes it clear where each name comes from. from math import sqrt still loads the whole module but makes just that name available in your program, so you can write sqrt(16), which is shorter but can clash with your own names.

Does random.randint include the upper bound?

Yes. random.randint(a, b) returns a whole number from a to b including both ends, so randint(1, 6) can return 6. random.randrange(a, b) stops before b, like range.

What does random.seed do?

It sets the starting point of the random number generator. With the same seed, the same sequence of "random" numbers is produced every run, which makes programs that use randomness repeatable for testing.

What does if __name__ == "__main__" mean?

Python sets __name__ to "__main__" in the file being run directly, and to the module's name in a file that is imported. Code under this if therefore runs only when the file is run on its own, not when another program imports it.

Why do I get AttributeError: module random has no attribute randint?

Usually because a file of your own named random.py is in the same folder, so import random loads your file instead of Python's module. Rename your file and delete any leftover random.py.

Suggest a change

Something here not clear? A topic you wish we covered? Tell us. We read every message, and a request is the fastest way to get a guide written — several of these exist because somebody asked.