Python Programming — Alpha School

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.py and import math loads *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 with random.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 random to be secure or fair by default. It is a simulation tool. For passwords, tokens or anything security-shaped, use the secrets module; for Lotteries of real money, use audited systems, not a seedable generator.
Try it. Try it: Use the statistics module: statistics.mean and statistics.median on the scores [78, 85, 92, 85, 70, 88, 85, 95, 72, 80]. You should get mean 83.0 and median 85.0. Lesson 19 rebuilds these from scratch so you understand what the library is doing for you.

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:

💪 Practice Zone

Try these yourself — hints are included, answers are not. Run your code in any Python environment.

  1. Your own module — Create mymath.py with double(n) and half(n) functions. In a second file, import mymath and use both.
    HintBoth files must live in the same folder. Run the second file from that folder.
  2. Freeze your environment — Create a virtual environment, install requests, and generate a requirements.txt with pip freeze.
    HintCommands: python -m venv env, activate it, pip install requests, pip freeze > requirements.txt.
  3. Standard library tour — Write a one-liner using each of: math.sqrt, random.choice, and datetime.date.today.
    HintEach needs an import first — 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

Keep reading to complete this lesson