A single file is fine up to a few hundred lines. Past that, finding anything becomes the hard part. Splitting a program into modules is mostly about making it navigable.
Warm-up: from Days 1 to 6
The last warm-up of the week, from memory first. It mixes days deliberately: the capstone at the end of today uses all of them at once.
1. Predict both lines.
from dataclasses import dataclass
@dataclass
class Habit:
name: str
minutes: list[int]
def total(self):
return sum(self.minutes)
habits = [Habit("read", [20, 30]), Habit("walk", [15]), Habit("code", [60, 45, 30])]
best = max(habits, key=lambda h: h.total())
print(best.name, best.total())
print([h.name for h in habits if len(h.minutes) > 1])Check your prediction
code 135
['read', 'code']key= can call a method: each habit is compared by its total. The comprehension
keeps the habits with more than one session.
2. Fix. Two playlists should each have their own songs: this should print
1 0.
class Playlist:
songs = []
def __init__(self, name):
self.name = name
def add(self, song):
self.songs.append(song)
a = Playlist("gym")
b = Playlist("sleep")
a.add("Run")
print(len(a.songs), len(b.songs))Check your fix
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add(self, song):
self.songs.append(song)
a = Playlist("gym")
b = Playlist("sleep")
a.add("Run")
print(len(a.songs), len(b.songs))1 0A list in the class body is one list shared by every instance. Built in
__init__, each playlist gets its own.
3. Write summary(expenses): given a dictionary of category → list of
amounts, it returns a dictionary of category → total. This should print
{'food': 20.75, 'travel': 30}:
# def summary(expenses): ...
print(summary({"food": [12.5, 8.25], "travel": [30]}))Check your answer
def summary(expenses):
return {category: sum(amounts) for category, amounts in expenses.items()}
print(summary({"food": [12.5, 8.25], "travel": [30]})){'food': 20.75, 'travel': 30}A dictionary comprehension over .items(): same keys, a new value for each. A
loop that fills an empty dictionary is just as good.
Several files, one program
Every playground on this page holds several files — use the tabs above the editor to move between them. Run always executes the entry file, marked with a dot.
Two files
from tax import with_tax looks for tax.py in the same folder. Everything
defined at the top level of that file — functions, classes, constants — is
importable.
Packages
A folder with an __init__.py is a package: a module that contains other
modules. It lets you group related files and gives the group a name.
Note from .cart import Cart inside __init__.py — the leading dot means
“from this package”, a relative import. Use relative imports between modules
of the same package, and absolute imports (from shop import Cart) from
outside it.
The main guard
Code at the top level of a module runs when it is imported. That is fine for definitions, and a problem for anything that acts.
Run it: the unguarded print fires during the import, while the guarded block
does not. __name__ is "__main__" in the file you executed and the module’s
own name everywhere else. Put your script’s actual work behind that guard, so
the file can be both imported and run.
Circular imports
If a.py imports b.py and b.py imports a.py, Python cannot finish either.
The fix is almost never a clever import trick — it is to notice that the two modules are really one concern split badly, or that a third module should hold what they share.
A layout that scales
myproject/
├── README.md
├── requirements.txt
├── src/
│ └── shop/
│ ├── __init__.py # what the package exposes
│ ├── cart.py # one concern per module
│ ├── pricing.py
│ └── cli.py # the entry point, behind a main guard
└── tests/
├── test_cart.py
└── test_pricing.py
Two rules carry most of the weight: one concern per module, and
dependencies point one way — cli imports cart, cart never imports
cli.
Before you write your own
Choose. No code for this one. In the layout above, where would each of these go — and which one is a trick question?
- A function that applies a discount code to a price.
- The
Cartclass. - The code that reads what the user typed and prints a receipt.
- An
import cliat the top ofcart.py, so the cart can print messages.
Check your choices
pricing.py— it’s a pricing rule, with no input or output.cart.py— one concern per module.cli.py— talking to the user is the entry point’s job, behind its main guard.- Nowhere. That makes
cartdepend oncliwhileclidepends oncart: a circular import. If the cart needs to report something, it should return it, and letclidecide how to show it.
Split a program up
The single file below does three things at once. Split it into temperature.py
(the conversions), report.py (the formatting) and main.py (the entry point,
behind a main guard). Use the file tabs to add code to each.
Run it once before you change anything and keep the output in view: after the split, it should print exactly the same four lines. That’s what makes this a refactor rather than a rewrite.
Hint 1: the idea
Sort the lines by job: which ones calculate, which ones turn numbers into
text, and which one decides what happens? Each job becomes a file. Each file
imports only from the one “below” it — main uses report, report uses
temperature — never the other way.
Hint 2: a first step
Move the two conversion functions into temperature.py unchanged, and put
from temperature import to_fahrenheit, to_kelvin where they used to be. Run
it: the output shouldn’t change. Then do the same with format_row and
report.py, and finally wrap the loop in main() behind the guard.
Show one solution
temperature.py holds the pure calculations, report.py turns numbers into
text, and main.py decides what to do — each importable without side effects:
# temperature.py
def to_fahrenheit(celsius):
return celsius * 9 / 5 + 32
def to_kelvin(celsius):
return celsius + 273.15# report.py
from temperature import to_fahrenheit, to_kelvin
def format_row(celsius):
return f"{celsius:6.1f}C {to_fahrenheit(celsius):6.1f}F {to_kelvin(celsius):6.1f}K"
def format_table(values):
return "\n".join(format_row(c) for c in values)# main.py
from report import format_table
def main():
print(format_table([-40, 0, 21.5, 100]))
if __name__ == "__main__":
main()Dependencies point one way — main → report → temperature — so
temperature.py can be tested without anything else existing.
What you learned
- Any
.pyfile beside yours is importable as a module. - A folder with
__init__.pyis a package;from .module import Xis a relative import. if __name__ == "__main__":separates “imported” from “run directly”.- Circular imports mean the split is wrong, not that you need a trick.
- One concern per module, and dependencies pointing one way.