Lesson 12: Modules, Libraries, and Virtual Environments
🎧 Listen to this lesson
No programmer writes everything from scratch. Python ships with a huge standard library — tested, documented modules for maths, dates, randomness, files, counting and more — and beyond it, an ecosystem of installable packages. This lesson teaches you to *use* that wealth properly: importing well, reading what a module offers, writing your own modules, and isolating projects with virtual environments so their dependencies never collide.
Importing: three styles, one rule
import math loads the whole module and you reach its contents through the module's name — math.sqrt(144). from math import sqrt brings one name directly into your code. import math as m shortens a long name. All three appear below; the rule is simple: prefer the plain import module form (readers always know where a name came from) and use from imports for a few obvious names from familiar modules. Never use from module import * in real code — it floods your namespace with names you cannot trace.
import math
from collections import Counter
print(math.sqrt(144))
print(math.ceil(4.2))
print(math.floor(4.8))
print(round(math.pi, 2))
print(Counter("banana"))
Output:
12.0
5
4
3.14
Counter({'a': 3, 'n': 2, 'b': 1})
Two details worth a beginner's attention. sqrt returns a float (12.0, not 12) because square roots are not always whole — library functions document their return types, and believing the documentation beats guessing. And Counter is Lesson 5's counting pattern industrialised: it tallied the string's characters in one call, most-common first in its display. That is the standard library's personality — the pattern you laboriously built by hand exists, perfected, one import away. Always check whether a module already does your task before writing forty lines of your own.
Randomness you can reproduce
random powers games and simulations, and it hides a debugging superpower: seed it, and the "random" sequence becomes identical on every run — so a bug that appeared once can be replayed exactly.
import random
random.seed(42)
print(random.randint(1, 100))
print(random.choice(["red", "green", "blue"]))
Output:
82
red
Run it again and you get 82 and red again — same seed, same sequence. (Lesson 9 deliberately left the quiz unseeded, so real players get a fresh order every game.) Simulations in science and the worked solution at the end of this lesson both rely on seeds: publish the seed with the results and anyone can reproduce the experiment — a core scientific value, not a programming trick.
Worked example 1 — dates without tears
Hand-rolled date arithmetic (month lengths, leap years) is a famous bug farm. The datetime module knows the calendar so you do not have to:
from datetime import date
today = date(2026, 10, 14)
new_year = date(2027, 1, 1)
print((new_year - today).days)
print(today.strftime("%d %b %Y"))
Output:
79
14 Oct 2026
Subtracting two date objects gives a timedelta, whose .days is the exact gap — 79 days, leap years and month lengths handled by the library. strftime formats a date for display (%d day, %b month name, %Y year). School and planner applications (Lesson 11's overdue logic, due dates, streaks) all stand on this module; its date.today() gives the real current date when a program needs it.
Worked example 2 — write your own module
A module is just a .py file. Put reusable functions in one file, import them in another, and the functions are written once and used everywhere. File 1 — greetings.py:
def hello(name):
return f"Hello, {name}!"
def goodbye(name):
return f"Goodbye, {name}!"
File 2 — main.py, in the same folder:
import greetings
print(greetings.hello("Ada"))
print(greetings.goodbye("Ada"))
Output:
Hello, Ada!
Goodbye, Ada!
(The two files were run exactly like this to produce that output.) Two things to notice. The functions live in greetings.py with no demo code glued to them — which is why Lesson 9's if __name__ == "__main__": guard matters: it lets a file be both a runnable program *and* an importable module without the program part firing on import. And the import worked because both files sit in the same folder — Python searches the current folder first, then its library paths. Structure a big project this way: one module per topic (planner_storage.py, planner_ui.py, …), and main.py conducting them.
Virtual environments and pip
Third-party packages (for charts, web frameworks, data science) are installed with pip, Python's package installer. Installing everything into your one system Python eventually creates conflicts — project A needs version 1 of a package, project B needs version 2. The professional solution is a virtual environment: a private Python installation per project. Create one with python -m venv .venv, activate it (the command differs slightly per operating system — your terminal prompt gains a (.venv) prefix), and then pip install requests installs into that project only. Record the project's dependencies in a requirements.txt file listing one package per line, such as requests==2.32.3 — a teammate (or you, on a new laptop) recreates everything with a single pip install -r requirements.txt. Treat the virtual environment as part of the project: never share it, never commit it, always be able to rebuild it from the requirements file.
Common mistakes
- Naming your file after a module. Save your homework as
math.pyandimport mathloads *your* file instead of the library — the error (AttributeError: module 'math' has no attribute 'sqrt') makes no sense until you realise the shadowing. Same trap withrandom.py,csv.py,json.py. Rename your file. - Importing and then wondering where a name came from. Weeks later,
from helpers import *leaves you searching the whole project for one function. Explicit imports are documentation. - Installing packages globally "to keep it simple". It is simple until two projects fight. The venv habit costs ten seconds per project and saves afternoons.
- Expecting
randomto be secure or fair by default. It is a simulation tool. For passwords, tokens or anything security-shaped, use thesecretsmodule; for Lotteries of real money, use audited systems, not a seedable generator.
Real-World Challenge
The task. Build a reusable dice module and a small program that uses it to run a fairness experiment: roll a six-sided die 1,000 times with a fixed seed and count how often each face lands. If the die is fair, each face should appear roughly 167 times.
Worked solution. File 1 — dice.py (the reusable module):
"""Reusable dice tools."""
import random
def roll(sides=6):
return random.randint(1, sides)
def roll_many(n, sides=6):
return [roll(sides) for _ in range(n)]
File 2 — roller.py (the experiment):
import random
from collections import Counter
import dice
random.seed(42)
rolls = dice.roll_many(1000)
counts = Counter(rolls)
for face in range(1, 7):
print(face, counts[face])
Output:
1 166
2 172
3 162
4 165
5 160
6 175
How it works. The module boundary is the lesson: dice.py knows how to roll (and defaults to six sides, while allowing twenty-sided dice for games with one parameter), and roller.py knows the experiment — it imports the tool, seeds the generator so the experiment is reproducible, collects 1,000 results and tallies them with Counter. The counts cluster around 166–175, close to the expected 166.7 per face: the generator behaves fairly at this sample size, and anyone can confirm it by running the same seed. Neither file does the other's job, so the dice module can be lifted into a board-game project unchanged tomorrow — reuse is not an accident, it is what the module structure was *for*.
Extension. Extend the module with roll_pair() returning two dice, then count the sums of 10,000 seeded pairs. Predict before running: which sum should dominate, and why does 7 beat 2? Compare Counter.most_common(1) against your prediction.
Going further — exploring modules and reading documentation
Nobody memorises the standard library; professionals *navigate* it. Three exploration tools work on any module, including ones you meet five minutes from now. dir() lists a module's names; filtering out the underscore-prefixed internals shows its public face. help() prints its documentation in the terminal. And every function's docstring is one attribute away:
import math
import json
print([name for name in dir(math) if not name.startswith("_")][:6])
print(math.gcd(24, 36))
print(json.dumps({"a": 1}, indent=2))
Output:
['acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2']
12
{
"a": 1
}
gcd surfaced from exactly this kind of browsing — you may never have been taught it, and now the greatest common divisor is a call away. (json.dumps above is Lesson 13's serializer in miniature: the "s" is for string, as with loads.) Beyond the terminal, the official documentation at docs.python.org is searchable, example-rich, and written for learners as much as experts; the professional skill is querying it well — module name plus the verb you want ("python json load from file") beats browsing aimlessly. Choosing third-party packages extends the same judgement: prefer packages that are maintained (recent releases), documented, widely used, and permissively licensed, and pin their versions in requirements.txt so today's exploration becomes tomorrow's reproducible setup. Curiosity with a method is a library card to everything Python can do.
Practice corner — knowing your environment
Two one-liners answer the questions every bug report should include — what Python is this, and do the library's numbers match my hand calculations from Lesson 19:
import sys
import statistics
print(sys.version_info[:2])
print(statistics.pstdev([2, 4, 4, 4, 5, 5, 7, 9]))
Output:
(3, 12)
2.0
The version tuple (here: Python 3.12) explains most "works on my machine" mysteries — a feature or fix present in one release is absent in another, which is why requirements and READMEs state a minimum version. The pstdev result, exactly 2.0 for this classic dataset, confirms the library agrees with the by-hand method — and from here on you may trust the library *because you have verified it*, which is the correct order of operations for every tool this course hands you.
🎬 Watch & Learn
Prefer video? These searches surface top tutorials on this lesson's topic:
- Python modules, pip & virtual environments — Corey Schafer — pick a recent, well-reviewed video
- freeCodeCamp: Python packages and pip explained — pick a recent, well-reviewed video
- Tech With Tim: virtual environments in Python — pick a recent, well-reviewed video
💪 Practice Zone
Try these yourself — hints are included, answers are not. Run your code in any Python environment.
- Your own module — Create
mymath.pywithdouble(n)andhalf(n)functions. In a second file,import mymathand use both.Hint
Both files must live in the same folder. Run the second file from that folder. - Freeze your environment — Create a virtual environment, install
requests, and generate arequirements.txtwithpip freeze.Hint
Commands:python -m venv env, activate it,pip install requests,pip freeze > requirements.txt. - Standard library tour — Write a one-liner using each of:
math.sqrt,random.choice, anddatetime.date.today.Hint
Each needs animportfirst — no pip install required for the standard library!
🌍 Real World
Every open-source Python project on GitHub ships a requirements.txt so anyone can recreate its environment with one command. Virtual environments are how professionals keep 50 projects' conflicting libraries from breaking each other on one machine.
Unit 4: Advanced Python
🎧 Listen to this unit