Session 21 of the Python Fundamentals series, covered in depth. It reuses code beyond a single file, covering imports from the standard library in all their forms - import math, from math import sqrt, import x as np, and from x import * - then writing and importing your own module, the if __name__ == '__main__' guard, and packages as folders of modules. It goes on to installing third-party code with pip and requirements.txt, isolating projects with virtual environments, and the import search path. The traps are ModuleNotFoundError, shadowing a standard-library name by calling your file random.py, and running work at import time. Every snippet and error message was executed and copied verbatim from CPython 3.12.
Subject: Python Fundamentals · 98 slides · code lesson
Open the interactive version of this deck · Homework for this lesson
Title
Python Fundamentals - Session 21
Stop rewriting code - borrow it, split it, and share it
Objectives
Until now every program lived in one file and used only built-in tools. This session opens the door to the rest of Python's code - and your own.
import a module and use it with dot notation (math.sqrt).from math import sqrt, and rename with as.if __name__ == "__main__": guard and why it matters.pip and pin them in requirements.txt.ModuleNotFoundError and shadowing a stdlib name.Warm-up
Discussion prompt
Before we open Session 21 - Modules, Packages & pip: without looking back, what was the main idea of Session 20 - CSV & JSON, and what could you do by the end of it that you could not do before?
Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.
Answer:
Session 20 of the Python Fundamentals series, in depth. Saving and loading structured data with two standard-library modules: json (dumps/loads for strings, dump/load for files, and the Python-to-JSON type map where True becomes true and None becomes null) and csv (reader/writer, DictReader/DictWriter, and the newline='' rule when you open the file).
Section
Part 1
Concept
Python ships with a huge standard library - hundreds of modules you never have to install. math, random, datetime, statistics are all there, waiting.
module — A file of Python code (functions, variables, classes) you can pull into your program with import. The standard library is a big collection of them.
Counterexample
Discussion prompt
Python ships with a huge standard library - hundreds of modules you never have to install. math, random, datetime, statistics are all there, waiting.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Concept
import math runs the module once and gives you a name (math) to reach its contents. You do this once, near the top of your file.
After that, everything inside lives behind the math. prefix.
Analogy
Discussion prompt
Explain import loads the module by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
import math runs the module once and gives you a name (math) to reach its contents. You do this once, near the top of your file.
Ranking
Put in order
Put the moves of import math and use it into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. One import at the top makes the whole math module available below.
Worked example
import math
print(math.sqrt(16))
print(math.pi)
print(math.floor(3.7))Line 1 loads the module
Why: One import at the top makes the whole math module available below.
Reach each tool through math.
Why: sqrt is a function; pi is a stored value; floor rounds down.
Read the output
Why: Verified by execution: 4.0, then 3.141592653589793, then 3.
| expression | prints |
|---|---|
| math.sqrt(16) | 4.0 |
| math.pi | 3.141592653589793 |
| math.floor(3.7) | 3 |
Comparison
Comparison matrix
From import math and use it: refill the prints column from what you know. The rest of the table is as it appeared.
| expression | prints |
|---|---|
| math.sqrt(16) | 4.0 |
| math.pi | 3.141592653589793 |
| math.floor(3.7) | 3 |
Concept
math.sqrt reads left to right: 'in the math module, the sqrt name'. The dot keeps names organized so two modules can each have a sqrt without clashing.
Explain it
Discussion prompt
Explain Dot notation: which module, which tool to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
math.sqrt reads left to right: 'in the math module, the sqrt name'. The dot keeps names organized so two modules can each have a sqrt without clashing.
Pattern
Predict first
The table runs: math.gcd(12, 18) | greatest common divisor | 6 · math.factorial(5) | 5 x 4 x 3 x 2 x 1 | 120 · math.ceil(4.1) | round up | 5
In One import, many tools, given the rows so far: what is the next one — the row where call is math.floor(4.9)?
Correct: math.floor(4.9) | round down | 4
| call | meaning | prints |
|---|---|---|
| math.gcd(12, 18) | greatest common divisor | 6 |
| math.factorial(5) | 5 x 4 x 3 x 2 x 1 | 120 |
| math.ceil(4.1) | round up | 5 |
| math.floor(4.9) | round down | 4 |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. gcd, factorial, ceil, floor all live in math - no extra imports needed.
Worked example
import math
print(math.gcd(12, 18))
print(math.factorial(5))
print(math.ceil(4.1))
print(math.floor(4.9))The single import unlocks all of them
Why: gcd, factorial, ceil, floor all live in math - no extra imports needed.
Trace each call
Why: Verified by execution: 6, 120, 5, 4. This table walks every call in the file.
| call | meaning | prints |
|---|---|---|
| math.gcd(12, 18) | greatest common divisor | 6 |
| math.factorial(5) | 5 x 4 x 3 x 2 x 1 | 120 |
| math.ceil(4.1) | round up | 5 |
| math.floor(4.9) | round down | 4 |
Trade off
Comparison matrix
From One import, many tools: every row here is a choice with a cost. Fill the meaning column, then say which row you would actually pick and what you give up for it.
| call | meaning | prints |
|---|---|---|
| math.gcd(12, 18) | greatest common divisor | 6 |
| math.factorial(5) | 5 x 4 x 3 x 2 x 1 | 120 |
| math.ceil(4.1) | round up | 5 |
| math.floor(4.9) | round down | 4 |
Section
Part 2
Concept
from math import sqrt copies just sqrt into your file. Now you write sqrt(25) with no prefix - handy when you use one tool a lot.
The trade-off: the reader no longer sees where sqrt came from.
Worked example
from math import sqrt, pi
print(sqrt(9))
print(pi)Import two names at once
Why: Separate them with commas; both land in your file directly.
Call them with no math. prefix
Why: Verified by execution: 3.0, then 3.141592653589793.
| you write | prints |
|---|---|
| sqrt(9) | 3.0 |
| pi | 3.141592653589793 |
Error analysis
Annotate
Walk the callouts on from math import sqrt, pi. Each one is a place this is easy to get subtly wrong.
Intuition
import math is standing in the hallway and reaching through the math. door each time. from math import sqrt is carrying sqrt out into your own room so it is right there.
Same tool, same result - just how far you reach for it. Reach through the prefix when you want the origin visible; bring it in when you use it constantly.
Concept
import numpy as np imports the module but calls it np in your file. Aliases save typing and follow community conventions (np, pd, plt).
alias — A shorter or conventional name for an imported module, set with as. import numpy as np means 'load numpy, but I'll call it np'.
Definition probe
Sort into buckets
Every line below is part of the definition of module or of alias — one or the other, never both. Put each where it belongs.
as.; import numpy as np means 'load numpy, but I'll call it np'.as. import numpy as np means 'load numpy, but I'll call it np'.Worked example
import math as m
print(m.sqrt(49))
print(m.pi)m is now the module
Why: The alias replaces the name math everywhere in this file.
Use the alias with dot notation
Why: Verified by execution: 7.0, then 3.141592653589793. Same module, shorter name.
| you write | prints |
|---|---|
| m.sqrt(49) | 7.0 |
| m.pi | 3.141592653589793 |
Blank canvas
Draw it
Draw what import math as m just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Concept
from math import * brings in every public name at once - sqrt, pi, floor, and dozens more - each with no prefix.
It reads short but hides origins and can quietly overwrite your own names. Convenient in a quick experiment; avoid it in real programs.
Worked example
from math import *
print(sqrt(16))
print(floor(2.9))Every math name is now loose in your file
Why: sqrt and floor work with no prefix - but so do dozens of names you never asked for.
Read the output
Why: Verified by execution: 4.0, then 2. It works, but a reader cannot tell these came from math.
| you write | prints |
|---|---|
| sqrt(16) | 4.0 |
| floor(2.9) | 2 |
Section
Part 3
Concept
There is nothing special about standard-library modules. Any .py file can be imported by another file in the same folder, using its name without the .py.
So a file mymath.py becomes import mymath next door.
Estimation
Predict first
Two files in one folder. First, mymath.py:
Commit before you compute: what does Split code into mymath.py come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: Running main.py prints three lines
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. Verified by execution: 'mymath loaded' (the import ran the print), then 7, then 20.
Worked example
Two files in one folder. First, mymath.py:
def add(a, b):
return a + b
def double(n):
return n * 2
print("mymath loaded")Then main.py imports it
Why: import mymath runs the whole file top to bottom, then hands you the mymath name.
| file | contents |
|---|---|
| mymath.py | two functions + a print |
| main.py | import mymath; call add, double |
Running main.py prints three lines
Why: Verified by execution: 'mymath loaded' (the import ran the print), then 7, then 20.
| source of the line | prints |
|---|---|
| import mymath (import-time print) | mymath loaded |
| mymath.add(3, 4) | 7 |
| mymath.double(10) | 20 |
Concept
The mymath loaded line proves it: import mymath executes the module top to bottom. Definitions get made, and any loose statements (like that print) actually run.
Import the same module twice and it still runs only once - Python caches it. Keep that print in mind; we will fix it with a guard shortly.
Sorting
Sort into buckets
These are the pieces of Session 21 - Modules, Packages & pip, out of order. Put each one back under the part of the lesson it belongs to.
Fill the middle
Fill in the blanks
From Pull just the functions you need — one line has had its right-hand side removed. Put it back.
from statistics import mean, median
scores = [72, 85, 90, 60]
print(mean(scores))
print(median(scores))
Why: scores is what everything below it consumes, so the wrong expression here fails later and somewhere else. statistics is a standard-library module; mean and median come in with no prefix.
Worked example
from statistics import mean, median
scores = [72, 85, 90, 60]
print(mean(scores))
print(median(scores))from-import works on stdlib and your modules alike
Why: statistics is a standard-library module; mean and median come in with no prefix.
Compute over the list
Why: Verified by execution: mean is 76.75, median is 78.5 (average of the middle two once sorted).
| call | prints |
|---|---|
| mean([72, 85, 90, 60]) | 76.75 |
| median([72, 85, 90, 60]) | 78.5 |
Section
Part 4
Concept
Python sets a hidden variable __name__ in every file. When you run a file directly, its __name__ is the string "__main__".
But when that same file is imported, its __name__ is the module's name instead. That one difference is the whole trick.
Worked example
A file greet.py that just prints its own __name__:
def greet(name):
return "Hello, " + name
print(__name__)Run greet.py directly
Why: Executed as the main program, so __name__ is "__main__".
Import greet from another file
Why: Verified by execution: run directly prints __main__; imported prints greet (then greet("Sam") gives Hello, Sam).
| how greet.py is used | print(__name__) shows |
|---|---|
| python greet.py (run directly) | __main__ |
| import greet (from another file) | greet |
Intuition
Think of __name__ == "__main__" as a file asking: 'Am I the star of the show right now, or just a supporting tool someone imported?'
Code under that guard runs only when the file is the star. Imported as a helper, the guarded block stays quiet - so importing never triggers a demo, a test, or a menu you did not ask for.
Concept
Put your 'run this file directly' code - demos, quick tests, a main menu - inside if __name__ == "__main__":. Keep your reusable defs outside it.
Now the file works two ways: run it to see the demo, or import it to borrow just the functions.
Ranking
Put in order
Put the moves of A guarded module into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. to_fahrenheit is defined whether the file is run or imported.
Worked example
def to_fahrenheit(c):
return c * 9 / 5 + 32
if __name__ == "__main__":
print(to_fahrenheit(100))The def is always available
Why: to_fahrenheit is defined whether the file is run or imported.
The guarded demo runs only when run directly
Why: Line 5 fires only if __name__ is "__main__".
Compare the two uses
Why: Verified by execution: run directly prints 212.0; imported, the guard is skipped, so nothing auto-runs and to_fahrenheit(0) gives 32.0 when you call it.
| how it is used | guard block | result |
|---|---|---|
| run directly | runs | 212.0 |
| imported | skipped | silent; to_fahrenheit(0) is 32.0 when called |
Section
Part 5
Concept
When a project grows past one file, group related modules in a folder. That folder is a package, and you reach into it with more dots: package.module.tool.
package — A folder that holds modules (and possibly sub-folders). Import a module out of it with dotted names, e.g. from shapes import circle.
Missing information
Discussion prompt
A folder shapes/ holding two modules, imported from a file beside the folder:
What do you need to know — or decide — before the first line can be written? List everything the problem has to hand you.
Hint: Anything you would have to invent to get started is a thing the problem must supply.
Answer:
shapes/circle.py and shapes/rectangle.py each define an area function.
Worked example
A folder shapes/ holding two modules, imported from a file beside the folder:
from shapes import circle, rectangle
print(round(circle.area(2), 2))
print(rectangle.area(3, 4))The folder is the package; the files are its modules
Why: shapes/circle.py and shapes/rectangle.py each define an area function.
| path | role |
|---|---|
| shapes/ | the package (folder) |
| shapes/circle.py | a module (uses math.pi) |
| shapes/rectangle.py | a module |
Call each module's area
Why: Verified by execution: circle.area(2) is 12.566..., rounded to 12.57; rectangle.area(3, 4) is 12.
| call | prints |
|---|---|
| round(circle.area(2), 2) | 12.57 |
| rectangle.area(3, 4) | 12 |
Comparison
Comparison matrix
From A shapes package: refill the role column from what you know. The rest of the table is as it appeared.
| path | role |
|---|---|
| shapes/ | the package (folder) |
| shapes/circle.py | a module (uses math.pi) |
| shapes/rectangle.py | a module |
Concept
shapes.circle.area is the same left-to-right reading, just deeper: package shapes, module circle, function area. Add folders, add dots.
This is exactly how big libraries are laid out - os.path.join, urllib.request.urlopen. Nothing new, just nesting.
Concept
Traditionally a package folder holds a file named __init__.py (often empty). Its presence tells older tools 'this folder is a package', and any code inside it runs when the package is first imported.
Modern Python can import some folders without it, but adding an empty __init__.py is the safe, universally understood habit.
Section
Part 6
Concept
The standard library is huge, but not everything. pip is Python's installer: it downloads third-party packages from PyPI (the Python Package Index) so you can import them.
pip — The command-line tool that installs third-party packages from PyPI into your Python. You run it in the terminal, not inside a .py file.
Estimation
Predict first
These are terminal commands (shell), not Python. The recommended form calls pip through the exact Python you are using:
Commit before you compute: what does Installing a package come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: python -m pip guarantees the right pip
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. It installs into the same Python that runs your code, avoiding 'installed but still ModuleNotFoundError' confusion.
Worked example
These are terminal commands (shell), not Python. The recommended form calls pip through the exact Python you are using:
python -m pip install requests
# then, inside your .py file:
# import requestspython -m pip guarantees the right pip
Why: It installs into the same Python that runs your code, avoiding 'installed but still ModuleNotFoundError' confusion.
| command | what it does |
|---|---|
| python -m pip install requests | download + install requests from PyPI |
| import requests | use it in your program afterward |
| python -m pip uninstall requests | remove it |
Reverse engineer
Discussion prompt
Work backwards. The example finished here:
python -m pip guarantees the right pip
What was it asked to do, and what must it have been given? Reconstruct the problem from its answer.
Hint: Every quantity in the result had to enter somewhere. Account for each one.
Answer:
These are terminal commands (shell), not Python. The recommended form calls pip through the exact Python you are using:
Concept
A requirements.txt file lists the packages (and versions) your project needs, one per line. Anyone can recreate your setup with one command.
It turns 'works on my machine' into 'works on any machine' - the file travels with your code.
Explain it
Discussion prompt
Explain requirements.txt pins your dependencies to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
A requirements.txt file lists the packages (and versions) your project needs, one per line. Anyone can recreate your setup with one command.
Missing information
Discussion prompt
A tiny requirements.txt, then the commands that create and consume it:
What do you need to know — or decide — before the first line can be written? List everything the problem has to hand you.
Hint: Anything you would have to invent to get started is a thing the problem must supply.
Answer:
freeze snapshots your current packages into the file; install -r reinstalls exactly that list elsewhere.
Worked example
A tiny requirements.txt, then the commands that create and consume it:
# requirements.txt
requests==2.31.0
numpy==1.26.4
# save what is installed:
# python -m pip freeze > requirements.txt
# install everything listed:
# python -m pip install -r requirements.txtfreeze writes, -r reads
Why: freeze snapshots your current packages into the file; install -r reinstalls exactly that list elsewhere.
| command | direction | effect |
|---|---|---|
| pip freeze > requirements.txt | write | record installed packages + versions |
| pip install -r requirements.txt | read | install every package listed |
| ==2.31.0 | pin | lock an exact version for reproducibility |
Section
Part 7
Concept
Install everything into one global Python and projects collide: project A needs an old version, project B needs a new one. A virtual environment gives each project its own private box of packages.
virtual environment — A self-contained folder with its own Python and its own installed packages, so one project's dependencies never disturb another's.
Matching
Match the pairs
Match each term to the definition this lesson gave it — not the one you would guess from the word.
as. import numpy as np means 'load numpy, but I'll call it np'.Why: These are the working definitions of module, alias, package, pip, virtual environment as Session 21 - Modules, Packages & pip uses them. Pairing them correctly is the test of whether you could state each one with the slide switched off.
Estimation
Predict first
Terminal commands. python -m venv builds the box; activating it points your terminal at that box's Python:
Commit before you compute: what does Create and activate a venv come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: Build, activate, then install
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. After activating, pip and python refer to the venv, so installs stay local to this project.
Worked example
Terminal commands. python -m venv builds the box; activating it points your terminal at that box's Python:
python -m venv .venv
# Windows:
.venv\Scripts\activate
# macOS / Linux:
source .venv/bin/activate
# now pip installs land inside .venv:
pip install requestsBuild, activate, then install
Why: After activating, pip and python refer to the venv, so installs stay local to this project.
| command | what happens |
|---|---|
| python -m venv .venv | create a private environment folder |
| .venv\Scripts\activate | activate it (Windows) |
| source .venv/bin/activate | activate it (macOS / Linux) |
| deactivate | leave the venv, back to global Python |
Reverse engineer
Discussion prompt
Work backwards. The example finished here:
Build, activate, then install
What was it asked to do, and what must it have been given? Reconstruct the problem from its answer.
Hint: Every quantity in the result had to enter somewhere. Account for each one.
Answer:
Terminal commands. python -m venv builds the box; activating it points your terminal at that box's Python:
Concept
The habit for any real project: create a venv, activate it, pip install what you need, then pip freeze > requirements.txt. Commit the requirements file, not the .venv folder.
A teammate clones your code, makes their own venv, and runs pip install -r requirements.txt - identical setup, zero collisions.
Analogy
Discussion prompt
Explain The everyday workflow by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
The habit for any real project: create a venv, activate it, pip install what you need, then pip freeze > requirements.txt. Commit the requirements file, not the .venv folder.
Section
Part 8
Concept
When you write import x, Python searches a list of folders in order and uses the first x it finds: your script's own folder first, then the standard library, then installed packages (site-packages).
sys.path — The ordered list of folders Python searches for imports. import sys; print(sys.path) shows it. First match wins.
Worked example
import sys
print(type(sys.path))
print(len(sys.path) > 0)It is an ordinary Python list
Why: You have used lists for sessions - this is one of them, holding folder-path strings.
Two facts you can rely on
Why: Verified by execution: type is <class 'list'>, and it is non-empty. Your script's folder is searched first - which sets up the next section's traps.
| expression | prints |
|---|---|
| type(sys.path) | <class 'list'> |
| len(sys.path) > 0 | True |
Blank canvas
Draw it
Draw what sys.path is just a list just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Intuition
Picture Python looking for a book. It checks your desk first (the script's folder), then the built-in shelf (the standard library), then the shelf of things you installed. It grabs the first copy it finds and stops.
Two consequences: if the book is nowhere, you get an error; and if a book on your desk has the same title as a library book, your desk copy wins. Both are the traps ahead.
Counterexample
Discussion prompt
Two consequences: if the book is nowhere, you get an error; and if a book on your desk has the same title as a library book, your desk copy wins. Both are the traps ahead.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Section
Part 9
Anomaly
Predict first
A student writes this, and it looks reasonable:
You import a package you never installed (or misspelled).
It is wrong. Say what breaks — and say it before you turn the page.
Correct: No superhelper on your desk, in the stdlib, or in site-packages - so the import itself fails before line 3.
Install it first (in your active venv), then import.
Why: No superhelper on your desk, in the stdlib, or in site-packages - so the import itself fails before line 3.
Trap
You import a package you never installed (or misspelled).
import superhelper
print(superhelper.run())Python searches every folder and finds nothing
Why: No superhelper on your desk, in the stdlib, or in site-packages - so the import itself fails before line 3.
| line | result |
|---|---|
| import superhelper | ModuleNotFoundError: No module named 'superhelper' |
Install it first (in your active venv), then import.
python -m pip install superhelper
# then in your .py file:
# import superhelperInstall, then the import resolves
Why: Real error, verbatim: ModuleNotFoundError: No module named 'superhelper'. The fix is install the package (check the spelling and that your venv is active), not edit the import line.
| cause | fix |
|---|---|
| not installed | python -m pip install <name> |
| misspelled name | correct the spelling |
| wrong environment | activate the venv that has it |
Error analysis
Annotate
Walk the callouts on Trap: ModuleNotFoundError. Each one is a place this is easy to get subtly wrong.
Anomaly
Predict first
A student writes this, and it looks reasonable:
You name your own file random.py, then import random elsewhere in the same folder.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: sys.path checks your folder first, so import random finds YOUR random.py - which has no randint - instead of the standard library's.
Never name a file after a module you import. Rename it.
Why: sys.path checks your folder first, so import random finds YOUR random.py - which has no randint - instead of the standard library's.
Trap
You name your own file random.py, then import random elsewhere in the same folder.
# your file is named random.py
import random
print(random.randint(1, 6))Your desk copy wins the search
Why: sys.path checks your folder first, so import random finds YOUR random.py - which has no randint - instead of the standard library's.
| line | result |
|---|---|
| import random | loads your random.py, not the stdlib one |
| random.randint(1, 6) | AttributeError: module 'random' has no attribute 'randint' |
Never name a file after a module you import. Rename it.
# rename your file to dice_game.py
import random
print(random.randint(1, 6))A non-colliding name lets the real module load
Why: Real error, verbatim: AttributeError: module 'random' has no attribute 'randint'. Once your file is renamed (and any stale random.pyc removed), the stdlib random loads and randint works.
| file name | import random finds |
|---|---|
| random.py | your file (no randint) - AttributeError |
| dice_game.py | the standard-library random |
Trade off
Comparison matrix
From Trap: shadowing a stdlib name: every row here is a choice with a cost. Fill the import random finds column, then say which row you would actually pick and what you give up for it.
| file name | import random finds |
|---|---|
| random.py | your file (no randint) - AttributeError |
| dice_game.py | the standard-library random |
Anomaly
Predict first
A student writes this, and it looks reasonable:
A module runs a demo (or asks for input) at the top level, with no guard.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: import runs the whole file, so that loose print executes just from importing - noise you never wanted in another program.
Put run-directly code behind the __name__ guard.
Why: import runs the whole file, so that loose print executes just from importing - noise you never wanted in another program.
Trap
A module runs a demo (or asks for input) at the top level, with no guard.
def to_fahrenheit(c):
return c * 9 / 5 + 32
print(to_fahrenheit(100))Importing it fires the print unexpectedly
Why: import runs the whole file, so that loose print executes just from importing - noise you never wanted in another program.
| action | unwanted effect |
|---|---|
| import this module | prints 212.0 during import |
Put run-directly code behind the __name__ guard.
def to_fahrenheit(c):
return c * 9 / 5 + 32
if __name__ == "__main__":
print(to_fahrenheit(100))Now importing is silent
Why: Verified by execution: run directly still prints 212.0; imported, the guard is skipped and nothing auto-runs. Definitions belong at top level; actions belong under the guard.
| how used | guarded version does |
|---|---|
| run directly | prints 212.0 |
| imported | nothing (silent) |
Comparison
Comparison matrix
From Trap: work that runs at import time: refill the guarded version does column from what you know. The rest of the table is as it appeared.
| how used | guarded version does |
|---|---|
| run directly | prints 212.0 |
| imported | nothing (silent) |
Anomaly
Predict first
A student writes this, and it looks reasonable:
You import with from, then call as if you used plain import (or the reverse).
It is wrong. Say what breaks — and say it before you turn the page.
Correct: You brought in sqrt only; the name math was never created, so math.pi has nothing to look up.
Match the call to the import form you chose.
Why: You brought in sqrt only; the name math was never created, so math.pi has nothing to look up.
Trap
You import with from, then call as if you used plain import (or the reverse).
from math import sqrt
print(math.pi)from math import sqrt never defines math
Why: You brought in sqrt only; the name math was never created, so math.pi has nothing to look up.
| line | result |
|---|---|
| math.pi (after from-import) | NameError: name 'math' is not defined. Did you forget to import 'math'? |
Match the call to the import form you chose.
from math import sqrt, pi
print(pi)Import what you call, call what you import
Why: Real error, verbatim: NameError: name 'math' is not defined. Did you forget to import 'math'? Either import pi too and call pi, or use import math and call math.pi. (The reverse - import math then bare sqrt(16) - gives NameError: name 'sqrt' is not defined.)
| import form | how you must call |
|---|---|
| import math | math.sqrt, math.pi |
| from math import sqrt, pi | sqrt, pi (no prefix) |
Two truths and a lie
Sort into buckets
Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.
math, random, datetime, statistics are all there, waiting.; import math runs the module once and gives you a name (math) to reach its contents. You do this once, near the top of your file.; math.sqrt reads left to right: 'in the math module, the sqrt name'. The dot keeps names organized so two modules can each have a sqrt without clashing.Section
Part 10
Pattern
1. Use the tool occasionally? import module
Why: The module. prefix keeps the origin visible: math.sqrt tells the reader where sqrt lives.
2. Use one or two names constantly? from module import name
Why: Pulls just those names in for short, clean calls - at the cost of hiding where they came from.
3. Long or conventional name? import module as alias
Why: import numpy as np: shorter to type and matches what every reader expects.
4. Avoid from module import * in real code
Why: It hides origins and can silently overwrite your own names. Fine for a throwaway experiment only.
Pattern
1. Put your reusable defs at the top level
Why: Functions and classes should be importable without side effects.
2. Move demos/tests under if __name__ == "__main__":
Why: So importing the file borrows the functions silently, while running it still shows the demo.
3. Name the file something that does not collide
Why: Never random.py, math.py, etc. - your folder is searched first and would shadow the real module.
4. For third-party needs: venv + pip + requirements.txt
Why: Isolate the project, install what you need, and pin it so anyone can reproduce the setup.
Real world
Discussion prompt
Outside this lesson: where does Session 21 - Modules, Packages & pip actually turn up? Name one concrete situation — a job, a piece of software someone ships, a decision somebody has to make — and say which part of Turning a script into a reusable module is doing the work in it.
Hint: Vague is the failure mode here. "Engineering" is not a situation; "deciding whether this build is fast enough to ship" is.
Answer:
Session 21 of the Python Fundamentals series, in depth. Reusing code beyond a single file: importing from the standard library (import math, from math import sqrt, import x as np, from x import *), writing and importing your own module, the if __name__ == '__main__' guard, packages as folders of modules, installing third-party code with pip and requirements.txt, isolating projects with virtual environments, and the import search path.
Check
Predict the output.
import math
print(math.sqrt(64))| call | prints |
|---|---|
| math.sqrt(64) | ? |
Check your understanding
What does this print?
Answer: A
Why: import math makes the module available; math.sqrt(64) returns 8.0 - sqrt always gives a float. Verified by execution.
Check
The import brings in sqrt only.
from math import sqrt
print(math.pi)| name imported | name used | result |
|---|---|---|
| sqrt | math | ? |
Check your understanding
What happens?
Answer: A
Why: from math import sqrt creates the name sqrt but never the name math, so math.pi raises NameError: name 'math' is not defined. Verified by execution.
Elimination
Eliminate the wrong options
When another file runs import this_module, what does the import print?
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: A
Why: When imported, __name__ is the module's name, not "__main__", so the if is False and the print never runs - the import is silent. Verified by execution.
Check
This file is imported by another file, not run directly.
def area(w, h):
return w * h
if __name__ == "__main__":
print(area(3, 4))| how used | guard block | output |
|---|---|---|
| imported | ? | ? |
Check your understanding
When another file runs import this_module, what does the import print?
Answer: A
Why: When imported, __name__ is the module's name, not "__main__", so the if is False and the print never runs - the import is silent. Verified by execution.
Check
You saved this file as random.py, in the same folder you run it from.
# file saved as random.py
import random
print(random.randint(1, 6))| file name | import random loads | result |
|---|---|---|
| random.py | ? | ? |
Check your understanding
What is the likely result?
Answer: A
Why: Your folder is searched first, so import random loads your own random.py, which has no randint, raising AttributeError: module 'random' has no attribute 'randint'. Verified by execution.
Check
The module is renamed on import.
import math as m
print(m.factorial(4))| alias | call | prints |
|---|---|---|
| m | m.factorial(4) | ? |
Check your understanding
What does this print?
Answer: A
Why: import math as m loads math under the name m, and factorial(4) is 4 x 3 x 2 x 1 = 24. Verified by execution.
Check
You wrote pip install requests inside your Python file and ran it.
pip install requests
import requests| line 1 | belongs in | result |
|---|---|---|
| pip install requests | ? | ? |
Check your understanding
Why does line 1 fail as written?
Answer: A
Why: pip install requests is a shell command you run in the terminal, not valid Python syntax; placing it in a .py file is a SyntaxError. Install in the terminal, then import in the file.
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — The import Statement · from, as, and ** · Writing Your Own Module · The __name__ Guard · Packages: Folders of Modules · pip* & requirements. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
You can reach beyond a single file: import the standard library, split your own code into modules, group them into packages, and pull in third-party code with pip.
| You write | It means |
|---|---|
| import math | load the module; use math.sqrt |
| from math import sqrt | bring in just sqrt (no prefix) |
| import numpy as np | load numpy, call it np |
| if __name__ == "__main__": | run this only when the file is run directly |
| python -m pip install X | install a package from PyPI (terminal) |
| python -m venv .venv | make an isolated environment for the project |
Watch the two big traps: ModuleNotFoundError (install it, or fix the spelling / venv) and shadowing (never name a file random.py). Next session we build on this to structure a real multi-file project.
Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.