python/volume-2/chapter-21ch 21 / 64 · 14 min

21Packages & program structure

In Chapter 20 we cracked the import machine open — sys.path, the chain of finders, the loaders, the .pyc cache — and watched a single module get found, compiled, and cached. Now we point that same machine at a whole directory. Your playlist has outgrown a single file. Shuffle logic, storage, and the data classes sprawl across a dozen .py files, and import shuffle sits next to import config with no hint of which belongs to whom. So we fold the pile under one nameimport playlist — the way import json hands you a whole folder of code through a single handle. Here's the plan. We'll see exactly what runs when a directory imports, meet __init__.py as runnable module code rather than ceremony, sort absolute imports from relative ones, learn what python -m quietly repairs, and watch two innocent imports poison each other. The whole way through we keep asking the one question that decides all of it — when a folder becomes a name, what code runs, and in what order? By the end we've restructured the whole playlist into a real package: one clean front door, the private plumbing hidden behind it, and python -m playlist starting the program from a single command.

A package is a directory that imports as one name __init__.py defines the public surface Absolute vs relative imports python -m and __main__.py: running a package
scroll to begin

01A package is a directory that imports as one name

Let's start with the mess. You've been writing one file per idea — engine.py, storage.py, model.py — and reaching across them with import storage. It worked, but the folder is now a junk drawer: nothing says these files are one thing. Meanwhile import json hands you a whole directory of standard-library code through a single word. That's exactly what you want here — import playlist, one name for the whole project. So here's the question, and it's sharper than it looks. How does a folder become a single importable thing, and what code — if any — runs the moment it does?

Watch what import actually does, because it isn't what most people picture. import playlist does not go hunting for a file called playlist. Instead it walks a list of finders — the objects sitting in sys.meta_path, the same crew you met last chapter — and asks each one the same question: do you know how to load the name 'playlist'? The finder that handles the filesystem, FileFinder, scans the directories on sys.path. The instant it spots a directory named playlist holding an __init__.py, it says yes. It hands back a module spec, a little record whose loader is a SourceFileLoader aimed straight at playlist/__init__.py. That's the handle the rest of the import is built on.

Now the import machinery runs a fixed four-step ritual. Every step matters later:

what import does, in orderpseudocode
# 1. create an empty module object — a namespace, nothing in it yet
mod = new_module("playlist")

# 2. mark it as a PACKAGE by giving it __path__
mod.__path__ = [".../playlist"]   # where future sub-imports will search

# 3. register it BEFORE running any of its code
sys.modules["playlist"] = mod    # remember this line — §5 turns on it

# 4. exec __init__.py's bytecode, using mod.__dict__ as its globals
exec(compile(init_source), mod.__dict__)

Read step 2 twice. The single thing that separates a package from an ordinary module is the attribute __path__ — a list holding the package's own directory. An ordinary module like math has no __path__. A package has one, and that list is the map future sub-imports consult when they need to find playlist's children. Step 4 is the reveal: __init__.py is not configuration, not a manifest, not boilerplate the interpreter reads for settings. It is the package's module body — ordinary Python that runs top to bottom, exactly like a script, with the fresh module's __dict__ serving as its global namespace. Whatever names it binds become the package's attributes. Print something at the top of __init__.py and it prints the instant someone imports the package.

playlist/__init__.pypython
print("init running")
print("__name__    =", __name__)      # playlist
print("__package__ =", __package__)   # playlist
print("__path__    =", __path__)      # ['/abs/path/to/playlist']

Here is the surprise that trips everyone: importing the package does not import its submodules. import playlist runs __init__.py and stops. There is no playlist.engine yet — touch it and you get AttributeError. A submodule springs into existence only when something explicitly imports it. At that moment the machinery does something quietly elegant: after loading engine.py, it binds the submodule onto its parent, so playlist.engine becomes a real attribute of the playlist module object. The package is a namespace that fills in on demand.

ON DISK — the folderAT RUNTIME — the module objectplaylist/__init__.pyengine.pystorage.py<module 'playlist'>__name__'playlist'__path__['.../playlist']__file__.../__init__.pyengine→ <module .engine>storage→ <module .storage>bound only after each submodule is importedexec fillsthis dict__path__ → where sub-imports search
Fig — Importing a package runs its __init__.py; the folder on disk becomes a live module object whose __path__ points back to the folder so submodule imports know where to look.
THE ONE IDEA TO CARRY FORWARD
A package is a directory the import system treats as one name, and __init__.py is that name's module body — real code that runs once, top to bottom, the first time anyone imports it. The folder is the filing cabinet; __init__.py is the code that decides what's in the drawer you actually open.

That sys.modules line in step 3 also explains why a package initialises exactly once. Import playlist twice in the same program and __init__.py runs a single time. On the second import, the machinery finds playlist already sitting in sys.modules and hands back the cached object without re-executing a line. Every module is a process-wide singleton keyed by its dotted name. This is the same cache that, from Ch 22 onward, you'll watch your program lean on as it starts reaching past its own memory.

Wait — a folder with no __init__.py still imports? Since Python 3.3, yes — it becomes a namespace package, and this is the one deliberate exception to everything above. Its __path__ is not a plain list but a special _NamespacePath that can span several directories on sys.path at once, and it has no __file__ and runs no init code, because there is no __init__.py to run. That's the point of them — letting one import name be assembled from folders shipped by different projects. For your own project, always write the __init__.py; you want the init hook and the single, unambiguous directory.

✗ The myth

"__init__.py is just a marker file — an empty file Python needs to notice the folder."

✓ The reality

It can be empty, but it is never inert. It is the package's module body, executed with the package's own namespace as globals. An empty one runs zero statements; a full one can import submodules, define the public API, and set state. Empty is a choice, not a requirement.

You have one name for the whole folder. But right now users can reach every file and every helper inside it. Next: how __init__.py lets you publish a clean front counter and hide the plumbing behind it. →

02__init__.py defines the public surface

Your users type from playlist import Track and it works. Good. But nothing stops them typing from playlist.engine._internal_shuffle import _fisher_yates — and now someone's code depends on a private helper you meant to rewrite next week. The moment they do, your freedom to change it is gone. You need a way to say, out loud and enforceable-in-spirit: this is the API I promise to keep; everything else is plumbing I reserve the right to move. Where does that line live, and how do you draw it?

The answer falls straight out of Section 1. A package's public surface is nothing more mystical than the set of names bound in the __init__ module's namespace after it finishes executing. There is no separate export table, no public: keyword. If a name is an attribute of the package module, it's reachable as playlist.that_name. If it isn't, it isn't. So you curate the surface by choosing what __init__.py binds. The tool is the re-export:

playlist/__init__.pypython
# pull the good names UP from the submodules where they live
from .model import Track, Playlist
from .engine import Player
from .storage import save, load

__all__ = ["Track", "Playlist", "Player", "save", "load"]

That from .engine import Player runs inside __init__.py, so it binds Player as an attribute of the playlist module. Now from playlist import Player resolves directly. The user never learns, and never needs to learn, that Player physically lives in engine.py. This is facade construction. Your folder layout is your implementation, free to be reorganised; the import path is your API, held stable. Move Player from engine.py into engine/core.py next year, fix the one line in __init__.py, and every caller keeps working. The facade decouples the two.

package: playlistfrom playlist import ___TrackPlayersavethe publiccounter__all__ =['Track','Player', 'save']taped to the import * door• • • frosted glass — internal, may change • • •engine.pystorage.py_fisher_yates_row_to_trackre-exportEverything is reachable asplaylist.storage._row_to_track — but only thecounter names are the promise you support.
Fig — __init__.py re-exports a small set of public names onto the front counter; the underscore-prefixed internals stay behind the glass, and __all__ governs only what import * grabs.

Now the two things people expect to be privacy controls — and neither one is. First, __all__. It is a list of strings, and it governs exactly one thing: what from playlist import * pulls into the caller's namespace. That's it. It does not hide anything. With the __all__ above, from playlist import * brings in those five names. But from playlist import debug_dump still works fine for any name that actually exists, listed or not. __all__ is a curated star-export manifest and a documentation signal ("these are the headline names"), never a lock.

Second, the leading underscore. Naming a helper _fisher_yates or _seed also changes exactly one behaviour: names starting with _ are skipped by import * when there's no __all__. Attribute access still reaches them perfectly — playlist.engine._fisher_yates reads out the function without complaint. The underscore is a message to humans and linters: touch this and you're on your own; I may delete it. Be exact about the mechanism, because half-truths here breed superstition. Python has no access control. There is no private keyword and no runtime that forbids reaching a name. "Private" in Python means precisely two non-enforcing things — omitted from __all__, and marked with an underscore — plus the social contract that goes with them. The interpreter will hand out every name you ask for. The convention is a fence, not a wall.

A FAT __init__.py IS A SLOW IMPORT
Every name you re-export runs its submodule's full import at package-import time. Re-export Player and plain import playlist now executes all of engine.py — its own imports, its top-level work, everything — whether the user wanted the player or not. A heavy __init__.py makes import playlist crawl, and, worse, it is the classic trigger for the import cycles of Section 5, because your __init__ now depends on every submodule finishing first.
Wait — can I keep the clean facade and a fast import? Yes — the labelled escape hatch is PEP 562, a module-level __getattr__. Define def __getattr__(name) in __init__.py and Python calls it only when an attribute is missing, so you can import the heavy submodule lazily, the first time playlist.Player is actually touched, instead of eagerly at package load. That's exactly how big libraries keep import numpy-style calls snappy. It is an advanced move — reach for it once your __init__ genuinely hurts, not before.
interactive · the eager-import taxdrag the slider
FIRST-IMPORT COST OF ONE LINE — import playlist 333 ms · 160 modules loaded … before your program runs a single line of its own playlist/__init__.py from .audio import * from .store import * from .model import * from .net import * from .plot import * from .frame import * from .learn import * from .report import * eager — every re-exported submodule executes now 333 ms lazy facade (PEP 562 __getattr__) — deferred until touched 3 ms 1 ms 10 ms 100 ms 1 s first-import time · log scale → eager is 111× slower & loads 155 more modules Opening the front door flips on every light in the building. A lazy facade lights only the room you actually walk into.
6
Each from .x import * in __init__.py runs that submodule's entire import at package-load time — its own import numpy, its top-level work, all of it — even for a user who only wanted Track. That is why a “fast” one-liner can detonate a cascade.
Fig — Modelled first-import cost on a cold cache (each heavy submodule pulls in a third-party dependency; real numbers depend on your dependencies). The eager bar is log-scaled — the gap between a lean facade and a fat one is a hundredfold, not a few percent.
↻ reframe
Stop thinking of __init__.py as "the file that makes it a package." Think of it as the package's storefront. The submodules are your workshop out back — messy, reorganised often, full of half-finished tools. __init__.py is the counter facing the street: it decides which finished goods go on display, under names that stay put even when you rebuild the whole workshop behind it.

You've published names. But those from .engine import Player lines with a leading dot are doing something specific — and they behave differently depending on how the file is run. Time to understand the dot. →

03Absolute vs relative imports

You reorganise. engine.py moves into a subfolder, and suddenly import storage throws ModuleNotFoundError — even though storage.py is right there next to it. You patch it to from .storage import save, and imported through the package, it works. Relieved, you run python engine.py to test that file alone — and it detonates: ImportError: attempted relative import with no known parent package. Same line, same file, opposite outcomes. You cannot move on until you understand why.

There are two import styles, and they resolve through completely different machinery. An absolute import names the full dotted path from a top-level entry on sys.path: from playlist.storage import save. The finder walks it segment by segment — find playlist, descend into it, find storage, pull out save. It depends on nothing but sys.path and the name.

A relative importfrom .storage import save, from ..util import log — never touches a filesystem name directly. The compiler encodes the leading dots as a level count: one dot is level=1, two dots level=2. At runtime the interpreter resolves the target in three moves. It takes the current module's __package__ string, walks level-1 dots up from it, then appends the name after the dots. The whole thing is relative to one attribute — __package__ — and hangs entirely on that string being set correctly.

how the dot resolvespython
# inside module  playlist.audio.engine   →  __package__ = "playlist.audio"
from ..util import log        # level = 2 (two dots)

# resolver: start at __package__      -> "playlist.audio"
#           strip (level-1)=1 name     -> "playlist"
#           append the target "util"   -> "playlist.util"
# result: from playlist.util import log

Now the crash makes sense. When you import playlist.engine, the machinery stamps engine.__package__ = "playlist", so .storage resolves to playlist.storage — clean. But when you run python engine.py, that file is loaded as the startup module. Its __name__ becomes "__main__" and its __package__ becomes "" — the empty string. Now the dot resolver is asked to walk up from nothing. There is no parent package string to strip a name from, so the interpreter can only raise: attempted relative import with no known parent package. The line isn't wrong. The context is missing. This is the exact hole that python -m, the next section, was built to fill.

A — imported as playlist.engineB — run as python engine.py__name__'playlist.engine'__package__'playlist'from .storage import savewalk 1 dot = 0 levels up from 'playlist'→ playlist.storagea known parent exists, so the dot has a base__name__'__main__'__package__'' (empty)from .storage import savethe dot has nothing to anchor toImportError: no parent packagea top-level script has no known parentlevel =# of dots
Fig — A relative import counts dots into levels and walks up from __package__; the exact same line resolves when the file is a package submodule but fails when it is run directly as __main__.

So which style should you use? The trade is real, and it's about who you're protecting. Relative imports say "my neighbour." from .storage import save means the sibling module in whatever package I live in. It never spells the top package's name, so if you rename playlist to setlist tomorrow, every relative import inside keeps working untouched. They make a package renamable and relocatable. Absolute imports say the full address. from playlist.storage import save is explicit and unambiguous, and it always resolves under -m. That's why it's the safe default for anything you might run as a program. The discipline that keeps you out of trouble is a single rule: never rely on a relative import in a file you also run directly as a script. For library code deep in the package, relative is lovely. For an entry point, use absolute, or run it through -m.

THE sys.path HACK IS A TRAP
The tempting "fix" for ModuleNotFoundError: storage is to bolt the folder onto sys.path so bare import storage resolves. It works — until your package is imported from anywhere else, at which point storage is either missing or, worse, collides with some other project's storage. You've turned a private submodule into a global name. Import it as what it is — from .storage import save, or from playlist.storage import save — and the ambiguity vanishes.
Wait — why does one dot mean "my own package" and not "one level up"? Because the dot count is a level, and level 1 means "strip zero names from __package__" — i.e. stay in the current package and look for a sibling. Two dots (level 2) strip one name and land in the parent package. So . is here, .. is up one, ... is up two. The number of dots is literally the number you feed the resolver as level, and it subtracts one before it starts walking.

If relative imports need a real package context, and running a file directly destroys that context, there must be a way to run a package as a package. There is — one command, and it's the front door your project has been missing. →

04python -m and __main__.py: running a package

You want your project to run with one clean commandpython -m playlist — not python playlist/some_file_i_happened_to_pick.py. And you're done with the relative-import crash from the last section. It turns out both problems have a single answer, and it's a flag you've probably typed without knowing what it repairs. To use it well, you need to see how differently CPython starts up in the two cases.

When you run python playlist/engine.py, the interpreter takes the script path. It sets sys.path[0] to the script's own directory (playlist/), then executes the file as the startup module — __name__ = "__main__", __package__ = "". That's the broken world of Section 3. There's no parent package, so every relative import inside dies. And because playlist/ itself is on the path but its parent isn't, import playlist may not even resolve.

When you run python -m playlist, a different subsystem — runpy — takes over, and it does three things in order:

what python -m playlist doespseudocode
# 1. put the CURRENT WORKING DIRECTORY on sys.path[0]
#    so the top-level package 'playlist' is importable
sys.path.insert(0, os.getcwd())

# 2. IMPORT the package normally -> runs playlist/__init__.py,
#    so 'playlist' is a real, correctly-initialised package
import playlist

# 3. find playlist/__main__.py and exec it AS the startup module,
#    but with package context preserved:
#      __name__    = "__main__"     (so the guard fires)
#      __package__ = "playlist"     (so relative imports RESOLVE)

Step 3 is the whole trick, and it's genuinely clever. The entry module runs under the name __main__, so if __name__ == "__main__": fires and it behaves like the program's start. And it carries __package__ = "playlist", so relative imports inside it resolve exactly as they would in any library module. Both truths hold at once: I am the program and I live inside a package. A plain script can never have both. That is why python -m is the invocation that makes from .engine import Player work in an entry point.

The file that runs is __main__.py, and now its role is clear. It is the package's designated entry point, the folder-level equivalent of the if __name__ == "__main__": block you'd put at the bottom of a single script. You don't pick a random file to be the front door. You write playlist/__main__.py, and python -m playlist finds it by convention.

playlist/__main__.pypython
from .engine import Player       # resolves under -m, would crash as a script
from .storage import load

print("__name__    =", __name__)     # __main__
print("__package__ =", __package__)  # playlist  — both true at once

tracks = load("library.json")
Player(tracks).run()
how do you start the app?python playlist/engine.pysys.path[0] = playlist/__name__ = '__main__'__package__ = ''from .model import Track ✗fragile entry pointpython -m playlist(runpy)1 — prepend cwd to sys.path2 — import playlist (runs __init__)3 — find playlist/__main__.py4 — exec as __main__ WITH__package__ = 'playlist'from .model import Track ✓clean entry point
Fig — Running a file directly strips its package identity, so relative imports break; python -m playlist routes through runpy, which imports the package first and executes __main__.py with __package__ set.

This also demystifies __name__ itself, the most cargo-culted line in Python. __name__ is simply the string the loader stamped on the module when it loaded it: "playlist.engine" when imported as a library, "__main__" when it's the startup module. So if __name__ == "__main__": is not magic. It's a plain string comparison asking one question: am I the program being run, or am I being imported by someone else? Run this only when I'm the program; stay quiet when I'm a library. The guard has meant that, and only that, all along.

-m IMPORTS THE PACKAGE FIRST
Note the order in step 2: python -m playlist runs __init__.py before it ever looks at __main__.py. So a syntax error or a bad import in __init__.py makes the whole thing fail before your entry point runs a single line. If python -m playlist dies with a traceback pointing at __init__.py, that's why — the package must construct itself before it can be run.
Wait — the standard library runs this way constantly. python -m json.tool pretty-prints JSON; python -m http.server serves a folder; python -m pip install ... is pip. Each is a package with a __main__.py front door. When you write your own, you're not inventing a pattern — you're joining the exact convention the standard library uses to make a package a runnable program.

✗ The myth

"python file.py and python -m pkg are two spellings of the same thing."

✓ The reality

They take different startup paths in CPython. The script form sets sys.path[0] to the file's folder and gives it no package context. The -m form imports the package first, then runs __main__.py with __package__ intact. Only one of them lets relative imports work in your entry point.

You have a package that imports as one name, publishes a clean surface, and runs from a real front door. One structural landmine remains — the one where two perfectly correct imports poison each other. →

05Circular imports and the half-built module

You split a class across two files — model.py needs a type from engine.py, engine.py needs a type from model.py — and import playlist dies with a line that sounds like an accusation, the circular import: ImportError: cannot import name 'Player' from partially initialized module 'playlist.engine' (most likely due to a circular import). Nothing is misspelled. Both files are correct in isolation. You could shuffle import lines until it stops crashing, but that's superstition. You want to know precisely why a valid pair of imports poisons each other, and the exact fix.

This builds directly on Section 1, on that one line you were told to remember: import inserts an empty module object into sys.modules before running the module's code. Watch the timeline of import playlist.model when the two files import each other at the top:

the poisoning, step by steptimeline
t0  import model      -> sys.modules["...model"]  = <empty>   # seed, before code
t1  model line 1:  from .engine import Player
t2    import engine   -> sys.modules["...engine"] = <empty>   # seed, before code
t3    engine line 1: from .model import Track
t4      model already in sys.modules  -> DO NOT re-run it

Read t4 slowly, because it's the crux. When engine does from .model import Track, the machinery finds model already in sys.modules — the empty seed from t0 — and, correctly, does not re-run it, since re-running would loop forever. It hands engine the half-built model object exactly as it stands. But at t4, model has only executed its first line; it never reached class Track. So the name Track isn't in model's namespace yet. from .model import Track demands that name right now, can't find it, and raises cannot import name 'Track' from partially initialized module. The module isn't broken. It's just unfinished, caught mid-execution by a neighbour who reached across too early.

model.pyengine.pysys.modules'pkg.model'EMPTY — Track not defined yet'pkg.engine'EMPTY — still running top levelt0inserted EMPTY into sys.modulest1from .engine import Playert2engine starts runningt3from .model import Trackfinds model half-builtImportError: cannot import 'Track'THE FIX — import at call time, not at import timedef play(self):from .model import Track # both module bodies finish first → ✓by the time the function runs, model is fully built in sys.modules
Fig — A circular import fails because Python caches a half-built module in sys.modules and the second module reads a name that isn't defined yet; deferring the import into a function body lets both bodies finish first.

Now the distinction that unlocks everything, and it's the difference between two import forms you probably thought were interchangeable:

two forms, two failure modespython
from .model import Track   # needs the NAME 'Track' to exist RIGHT NOW, at import time
import playlist.model     # binds the MODULE object; you touch .Track LATER, at call time

from .model import Track copies a name out of model's namespace at the instant the import runs — during module execution, when model may still be half-built. import playlist.model only binds the module object, which exists from t0, empty or not. You don't read model.Track until later, at call time, by which point both module bodies have finished and Track is present. So the cycle itself is not fatal. What's fatal is depending on a not-yet-executed name during module execution. Change when you need the name, and the cycle becomes harmless.

That gives four fixes, worst to best:

fixes, in order of preferencepython
# 1. move the import to CALL time — runs after both bodies finished
def shuffle(self):
    from .model import Track   # local import, deferred until called
    ...

# 2. bind the module, read the attribute lazily
import playlist.model
# ...later...  playlist.model.Track

# 3. type-checking only — exists for the checker, never at runtime
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from .model import Track   # used only in string annotations

# 4. BEST — break the cycle: extract Track into a leaf module both import
THE ONE IDEA TO CARRY FORWARD
Module bodies run top to bottom, as code. A cycle is only safe if nobody reaches across it for a name before both sides have finished executing. from X import name reaches at import time (fragile); import X then X.name reaches at call time (safe). You don't fix circular imports by shuffling lines — you fix them by deferring the reach or removing the loop.
Wait — why does moving the same import inside a function cure it? Because a function body doesn't execute when the module loads — only when the function is called. By the time shuffle() is actually invoked, import playlist has long finished, both model and engine are fully built, and Track is sitting in model's namespace waiting. Same line, later moment — and the moment was the whole problem.

Every mechanism is now on the table: the package name, the facade, the dot, the front door, the cycle. Time to cash them all in — turn the sprawling playlist into a real, layered, cycle-proof program. →

06Restructuring the playlist into a package

Everything so far has been mechanism. Now you spend it. The playlist has swollen into one bloated file mixing data classes, playback logic, and file-saving. You can't test the storage in isolation, can't hand a teammate "just the engine," can't change the save format without risking the shuffle. You want to convert the pile into a program: a package with clear modules, one public surface, one entry point, and — by construction — no import cycles. This is the exact shape the capstone in Ch 23 will ship.

The single design decision that makes all of it fall into place is dependency direction. Decide which module depends on which, and make every arrow point the same way — toward a foundation. Here's the layout:

playlist/ — the packagetree
playlist/
  __init__.py     # facade: re-exports the public names + __all__
  model.py        # LEAF — Track, Playlist. imports nothing internal
  storage.py      # imports model. save/load to JSON
  engine.py       # imports model. Player, shuffle, next/prev
  __main__.py     # entry point: wires storage + engine. python -m playlist

Look at the arrows. model.py is a leaf: pure data — the Track and Playlist dataclasses — importing nothing else in the package. storage.py imports model, one direction. engine.py imports model, one direction. Neither storage nor engine imports the other; they meet only at the top, in __main__.py, which wires them together. Because every arrow points down toward the model leaf, there is no cycle possible — not "we avoided one," but "the shape forbids one." This is the deep lesson of Section 5 turned into architecture. You don't dodge circular imports with clever import placement; you make them structurally impossible by layering.

the import lines that enforce the layerspython
# model.py   — the leaf, imports nothing internal
from dataclasses import dataclass

# storage.py — one arrow down to model
from .model import Track, Playlist

# engine.py  — one arrow down to model
from .model import Track, Playlist

# __init__.py — the facade on top
from .model import Track, Playlist
from .engine import Player
from .storage import save, load
__all__ = ["Track", "Playlist", "Player", "save", "load"]

Every prior mechanism is doing a job here, in situ. The relative imports (from .model import ...) mean the package is renamable and works under -m — Section 3. Those imports resolve because __package__ is set correctly when the package is imported, and __main__.py keeps that context under python -m playlist — Section 4. __init__.py re-exports the public trio behind a stable facade and declares __all__ — Section 2. And the acyclic layering is the structural cure for the poisoning of Section 5. None of the modules is meant to be run directly. The only front door is __main__.py.

__init__.pyfacade — re-exports + __all____main__.pyentry — python -m playlistTOPstorage.pyengine.pyMIDmodel.pyleaf — no internal importsBASEseam:swap → SQLiteseam:swap → strategy(Ch 23)all arrows point down → no cycle possible
Fig — Layering the package so every import points downward — entry and facade on top, engine/storage in the middle, a dependency-free model at the base — makes circular imports structurally impossible and leaves clean seams to swap later.

The payoff is the seams, and this is why the layering is worth the ceremony. Because storage.py isolates all the I/O, you can swap JSON for SQLite later without touching a line of engine.py. The engine never knew where tracks came from, only that load() returns them. Because engine.py holds the algorithm, you can swap the shuffle strategy without touching storage. And model.py is the stable contract both sides agree on — the Track shape that storage produces and engine consumes. Layering doesn't just prevent cycles; it hands you a program you can change one piece at a time. That's exactly the surface Ch 23's capstone builds on.

THE ONE IDEA TO CARRY FORWARD
Structure is not decoration. A directed dependency graph pointing at a leaf, relative imports for renamability, a curated facade, and a single __main__.py front door are the four things that convert a folder of files into a program. Get the arrows pointing one way and the hardest bug in this chapter — the circular import — can never occur.

✗ The myth

"A package is just a folder with the files thrown in. Structure is tidiness — nice to have, not load-bearing."

✓ The reality

The direction of the imports is load-bearing. Point every arrow at a leaf and cycles are impossible, testing-in-isolation is free, and each module has a stable interface you can swap behind. The same files with the arrows tangled give you a fragile knot that crashes on import. Structure is the program.

Wait — this is the last time it's this easy. Every boundary in this package is a name in a dict pointing at a real object in this one process's RAM — engine holds the actual Track object, not a copy. From Ch 22 on, the boundaries your program crosses are the disk, the network, and other processes, and none of them can carry a live object across. Only bytes survive the crossing. This clean, in-memory layering is the ground you'll stand on while you learn to flatten objects down to bytes and rebuild them on the far side.

Your program has a shape. Next volume-thread: the moment it needs to speak to anything outside itself — a file, a socket, another process — the objects stop crossing and the bytes begin. That's Ch 22. →

PROGRAMS run them line by line — the same ideas, written the many ways Python offers
Chapter 21, in working code

A package is just a folder Python agrees to treat as a namespace. Each program here builds a tiny package on disk at runtime. It writes the .py files, puts the folder on sys.path, then imports it, so every effect is real: the files exist, the imports actually run, and the errors are Python's own. Watch __init__.py fire exactly once, the public surface assemble through re-exports, relative imports resolve against __package__, and the circular-import crash appear and then get fixed.

A folder becomes a package
The presence of __init__.py is what turns a directory into a regular package; its top level runs once, on first import, and is then cached. Importing the package is not the same as importing its submodules, and a folder with no __init__.py becomes a different beast — a namespace package.
The public surface
A package chooses what to show. __init__.py can re-export names from submodules so users import them from one clean place, and __all__ (plus the leading-underscore convention) decides what a wildcard import copies out — the package's front door versus its plumbing.
Relative imports & __package__
Inside a package, a leading dot means 'the package I live in'. That resolution is driven by each module's __package__, which differs between a package's __init__ and its submodules — and which is empty when a file is run as a loose script, the exact condition that makes relative imports fail.
Running packages & breaking cycles
A package can be a runnable program via __main__.py, executed with 'python -m'. And when two modules import each other at the top level, the second one runs into a half-built first — the partially-initialized-module crash — which a deferred, call-time import cleanly avoids.
end of chapter 21 · six sections
iolinked.com
Written by Ajai Raj