◈ python mapVol 2 · Ch 21/30
Volume 2 Python, from the metal up · chapter 21

21Packages & program structure

In Chapter 20 we cracked the import machine open. We met sys.path, the chain of finders, the loaders, and the .pyc cache, then watched a single module get found, compiled, and cached. In this one we point that same machine at a whole directory. Your playlist has outgrown a single file. Shuffle logic, storage, and the data classes now 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 name, import 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, and meet __init__.py as runnable module code rather than ceremony. Then we'll 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. There's one clean front door, the private plumbing hidden behind it, and python -m playlist starting the program from a single command.

iolinked · chapter 21 — the checkpoints6 steps
$ sections covered in Packages & program structure
01A package is a directory that imports as one name
02__init__.py defines the public surface
03Absolute vs relative imports
04python -m and __main__.py: running a package
05Circular imports and the half-built module
06Restructuring the playlist into a package

01A package is a directory that imports as one name

Let's start with the mess. You've been writing one file per idea, like engine.py, storage.py, and 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. It walks a list of finders, the objects sitting in sys.meta_path, the same crew you met last chapter. It 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, and 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. The fresh module's __dict__ serves 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']

And here is the surprise that trips everyone: importing the package does not import its submodules. import playlist runs __init__.py and stops, so there is no playlist.engine yet — touch it and you get AttributeError. A submodule springs into existence only when something explicitly imports it, and 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.

SYNTAX · making a package — a directory, an __init__.py, one namethere is no build step and no registry: the folder is importable the moment it holds that file
myproject/ jukebox/ -- THE PACKAGE. a directory, nothing more. __init__.py -- the package's module body. may be empty. tracks.py -- a submodule -> jukebox.tracks run.py -- your code, one level UP from the package import jukebox -- runs jukebox/__init__.py. Once. And stops. import jukebox.tracks -- runs __init__.py FIRST, then tracks.py from jukebox import tracks -- the same two files; only the local name differs jukebox.__path__ -- ['.../jukebox'] the mark of a package jukebox.tracks -- AttributeError until something imports it
the directoryThe whole mechanism. A folder named jukebox holding an __init__.py is a package. Nothing to declare, nothing to install.
__init__.pyNot a marker file. It is the package's module body, executed once with the package's own namespace as its globals.
__path__The single attribute that separates a package from a module: a list holding the package's directory, and the map every sub-import consults.
where you standsys.path must contain the package's parent, never the package itself. Standing in myproject is what makes index 0 correct.
import jukeboxRuns __init__.py and stops. It does not walk the folder, and it imports zero submodules.
import jukebox.tracksImports the parent first, always. Then it binds tracks as a real attribute of the live jukebox module object.
the second importA sys.modules hit. Nothing re-runs, and both names point at the same module object in this process.
you type
# ---------- jukebox/__init__.py ----------
print("jukebox/__init__.py running")


# ---------- jukebox/tracks.py ----------
print("jukebox/tracks.py running")

TITLES = ["Blinding Lights", "Titanium", "Levels"]


# ---------- run.py  (one level UP from the package) ----------
import os
import sys

import jukebox                      # runs jukebox/__init__.py -- once

print("name        :", jukebox.__name__)
print("__file__    :", "/".join(jukebox.__file__.replace(os.sep, "/").split("/")[-2:]))
print("__path__    :", [os.path.basename(p) + "/" for p in jukebox.__path__])
print("has tracks? :", hasattr(jukebox, "tracks"))

import jukebox.tracks               # NOW the submodule runs

print("has tracks? :", hasattr(jukebox, "tracks"))
print("bound onto  :", jukebox.tracks.__name__)
print("same object :", sys.modules["jukebox.tracks"] is jukebox.tracks)
print("first title :", jukebox.tracks.TITLES[0])

import jukebox                      # cached: not one line re-runs
print("import #2   : nothing printed -- sys.modules hit")


# ---------- the terminal, standing in myproject/ ----------
$ python run.py
$ python -c "import jukebox; jukebox.tracks"
you see
$ python run.py
jukebox/__init__.py running
name        : jukebox
__file__    : jukebox/__init__.py
__path__    : ['jukebox/']
has tracks? : False
jukebox/tracks.py running
has tracks? : True
bound onto  : jukebox.tracks
same object : True
first title : Blinding Lights
import #2   : nothing printed -- sys.modules hit

$ python -c "import jukebox; jukebox.tracks"
jukebox/__init__.py running
Traceback (most recent call last):
  File "<string>", line 1, in <module>
AttributeError: module 'jukebox' has no attribute 'tracks'
where beginners trip
  • Three import jukebox statements, one jukebox/__init__.py running. That is the cache from Chapter 20, not a trick.
  • The AttributeError is the rule, not the bug. import jukebox never opened tracks.py, so there is nothing to attach.
  • Run from inside jukebox/ and nothing resolves. The parent directory is the one that has to be on sys.path.
  • An empty __init__.py is a complete one. A module body with zero statements is still a module body.
  • Since 3.3 a folder without __init__.py imports anyway, as a namespace package. Write the file regardless — you want the hook and the single directory.
  • from jukebox import tracks and import jukebox.tracks execute the identical two files. Only the name left in your namespace differs.
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, in 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, and 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, while 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.

SYNTAX · the three jobs of __init__.py — and the two it cannot doempty is legal, the re-export builds the facade, and __all__ curates import * and nothing else
# jukebox/__init__.py -- three shapes, all legal # 1. EMPTY. zero statements. a complete package. # 2. RE-EXPORT -- lift the good names UP out of the submodules from .tracks import Track from .storage import load, save # 3. DECLARE -- the star-export manifest, and metadata __all__ = ["Track", "load", "save"] __version__ = "1.0.0" # what it buys the caller from jukebox import Track -- resolves, though Track lives in tracks.py from jukebox import * -- exactly the three names in __all__ from jukebox.storage import _tmp_name -- STILL WORKS. __all__ is not a lock.
empty is a choiceAn empty __init__.py publishes nothing and costs nothing. Reach for re-exports when there is an API you are willing to promise.
from .tracks import TrackBinds Track as an attribute of the package. from jukebox import Track then resolves, and the caller never learns where the class lives.
the facadeYour folder layout becomes implementation. Move Track into tracks/core.py next year, edit one line here, and every caller survives.
__all__Governs exactly one thing: which names from jukebox import * copies out. It hides nothing and forbids nothing.
the leading _Also exactly one thing: import * skips it when there is no __all__. Attribute access reaches it every single time.
the submodule staysfrom .tracks import Track imports jukebox.tracks, so jukebox.tracks is reachable too. A facade adds a door; it removes none.
the costEvery re-export runs its submodule in full, at package-import time. A fat __init__.py is a slow import jukebox.
you type
# ---------- jukebox/tracks.py ----------
"""tracks.py -- what a track IS. No I/O."""
from dataclasses import dataclass


@dataclass
class Track:
    title: str
    artist: str
    seconds: int

    def __str__(self):
        minutes, secs = divmod(self.seconds, 60)
        return f"{self.title} - {self.artist} ({minutes}:{secs:02d})"


# ---------- jukebox/storage.py ----------
"""storage.py -- the only file that knows what JSON is."""
import json

from .tracks import Track


def save(tracks, path):
    with open(path, "w", encoding="utf-8") as f:
        json.dump([vars(t) for t in tracks], f)
    return path


def load(path):
    with open(path, "r", encoding="utf-8") as f:
        return [Track(**row) for row in json.load(f)]


def _tmp_name(path):                 # plumbing: not part of the promise
    return path + ".part"


# ---------- jukebox/__init__.py ----------
"""jukebox -- a tiny music library."""
from .tracks import Track
from .storage import load, save

__all__ = ["Track", "load", "save"]
__version__ = "1.0.0"


# ---------- surface.py ----------
import jukebox

print("__version__ :", jukebox.__version__)
print("__all__     :", jukebox.__all__)

ns = {}
exec("from jukebox import *", ns)
print("import *    :", sorted(n for n in ns if not n.startswith("__")))

print("same class  :", jukebox.Track is jukebox.tracks.Track)
print("submodule   :", jukebox.tracks.__name__, "| still reachable")
print("private     :", jukebox.storage._tmp_name("library.json"))

ns = {}
exec("from jukebox.storage import *", ns)      # storage has NO __all__
print("storage star:", sorted(n for n in ns if not n.startswith("__")))
you see
$ python surface.py
__version__ : 1.0.0
__all__     : ['Track', 'load', 'save']
import *    : ['Track', 'load', 'save']
same class  : True
submodule   : jukebox.tracks | still reachable
private     : library.json.part
storage star: ['Track', 'json', 'load', 'save']

$ python -c "from jukebox import Track; print(Track('Titanium', 'David Guetta', 245))"
Titanium - David Guetta (4:05)

$ python -c "from jukebox.storage import _tmp_name; print(_tmp_name('a.json'))"
a.json.part
where beginners trip
  • Read storage star twice. With no __all__, import * also exported json and Track — names storage merely imported.
  • _tmp_name escaped that star import only because of its underscore, and it is still one dotted attribute away.
  • __all__ is a list of strings. A typo in it is silent until someone runs import * and gets an AttributeError.
  • Re-exporting Track also binds jukebox.tracks. The submodule never becomes invisible, and never can.
  • Every name in __all__ costs an import at package load. Three light names are free; six heavy ones are a second of startup.
  • __version__ is a convention, not a hook. No part of Python reads it; the packaging tools read pyproject.toml.
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. So __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, so 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, because the convention is a fence, not a wall.

Here's why the facade earns its keep, in milliseconds. Suppose __init__.py eagerly imports all six submodules, and each drags in a heavy dependency costing about 200 ms on a cold cache. That's 6 × 200 = 1200 ms — over a second — burned before import playlist even returns. A lean facade that re-exports three light names pays perhaps 5 ms, and defers the heavy work until something actually reaches for it.

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.

⚠ MOST BEGINNERS THINK…the folder on disk has no idea what a package is
Right — I made the folder. jukebox/ holds an __init__.py, and section 1 said that is all it takes, so on disk this is a package. Which means report.py, sitting in there beside it, is part of the package — the interpreter can see the __init__.py lying right next to the file it is running, so when I write from .tracks import LIBRARY the dot has something obvious to point at. And if python jukebox/report.py still complains about a missing parent package, then it is a directory problem: I am standing in the wrong place, and either a cd or one line of sys.path will sort it out.
TYPE THIS — 10 SECONDS
# ---------- jukebox/__init__.py ----------
print("jukebox/__init__.py running")

# ---------- jukebox/tracks.py ----------
LIBRARY = ["Blinding Lights", "Titanium", "Levels"]

# ---------- jukebox/report.py ----------
print("report __package__ =", repr(__package__))

from .tracks import LIBRARY

print("report sees", len(LIBRARY), "tracks")

# ---------- standing in myproject/, launch the SAME file two ways ----------
$ python jukebox/report.py
$ python -m jukebox.report
$ python jukebox/report.py
report __package__ = None
Traceback (most recent call last):
  File "C:\tmp\myproject\jukebox\report.py", line 3, in <module>
    from .tracks import LIBRARY
ImportError: attempted relative import with no known parent package

$ python -m jukebox.report
jukebox/__init__.py running
report __package__ = 'jukebox'
report sees 3 tracks
Two launches, one unedited file, one unchanged folder — so whatever differs is not on the disk: a package context is a string the import machinery stamps on a module at the moment it imports it under a dotted name, and launching a file performs no import at all, so there is nothing to do the stamping. Read the missing line first, because it is the loudest thing in that output. Run 1 never printed jukebox/__init__.py running. The package was not merely nameless — it never came into existence: nothing imported it, its module body never ran, and there was no live jukebox object in that process for a dot to be relative to. Run 2 printed it before anything else, because -m imports the package first and only then runs your file. Now read the field. One file nobody edited reports None in the first run and 'jukebox' in the second, and that second value came from the dotted name you typed, jukebox.report — not from the folder the file happens to sit in. Which is also why the second half of the belief dies quietly: run the script form from any directory on the disk and it fails identically, because that form never asks the working directory a question at all. One honest refinement, since section 3 rounds this off: on 3.12 the script form leaves __package__ as None, not the empty string. '' is what a genuine top-level imported module carries — a bare import settings gives you exactly that. The resolver turns both away for the identical reason, no base to strip a name from, which is why the distinction never changes what you do about it.

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 import, like from .storage import save or 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 from the current module's __package__ string. It 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.

Count it yourself, because the dots are just arithmetic. Say playlist/engine/core.py is imported, so its __package__ is "playlist.engine". Now from ..model import Track carries two dots, so level=2. The resolver walks level-1 = 1 dot up from playlist.engine, landing on playlist. Then it appends model, giving playlist.model. Every dot past the first is one step up the tree.

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. 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__.
SYNTAX · the dot — relative imports, and the one case where they are illegalthe dots are arithmetic on __package__; launch the file as a script and there is nothing to do the arithmetic on
# inside jukebox/report.py -> __package__ == "jukebox" from . import tracks -- the sibling MODULE object from .tracks import as_clock -- a NAME out of the sibling # inside jukebox/formats/m3u.py -> __package__ == "jukebox.formats" from ..tracks import as_clock -- level 2: strip one name, then append # the same target, absolute -- the full address from a sys.path entry from jukebox.tracks import as_clock # the resolver, in three lines. that is the entire feature. level = the number of dots base = __package__ with (level - 1) trailing names stripped target = base + "." + the name written after the dots
. is hereOne dot is level 1, and level 1 strips nothing. .tracks means my sibling, never my parent.
.. is up oneTwo dots strip one name off __package__. Every extra dot is one more step up the tree, and no dot count means "the root".
__package__The only input to the whole calculation. A plain string the loader stamped on the module when it loaded it.
run as a script__package__ becomes "". There is no base to strip from, so the interpreter raises rather than guess at your layout.
the error textattempted relative import with no known parent package names the missing thing precisely. It is a context error, never a spelling error.
relative buys renamingfrom .tracks import ... never spells jukebox. Rename the folder and every internal import keeps resolving, untouched.
absolute buys clarityfrom jukebox.tracks import ... reads as an address, always resolves under -m, and is the safe default in an entry point.
you type
# ---------- jukebox/report.py ----------
from . import tracks                 # the sibling MODULE object
from .tracks import as_clock         # a NAME out of the sibling

print("__name__    =", __name__)
print("__package__ =", __package__)
print("total       =", as_clock(785), "|", tracks.as_clock(785))


# ---------- jukebox/formats/m3u.py ----------
from ..tracks import as_clock        # level=2: strip one name, then append

print("__name__    =", __name__)
print("__package__ =", __package__)
print("line        =", "#EXTINF:" + as_clock(245))


# ---------- the terminal, standing in myproject/ ----------
$ python -m jukebox.report
$ python jukebox/report.py
$ python -m jukebox.formats.m3u
you see
$ python -m jukebox.report
__name__    = __main__
__package__ = jukebox
total       = 13:05 | 13:05

$ python jukebox/report.py
Traceback (most recent call last):
  File "C:\tmp\myproject\jukebox\report.py", line 2, in <module>
    from . import tracks                 # the sibling MODULE object
    ^^^^^^^^^^^^^^^^^^^^
ImportError: attempted relative import with no known parent package

$ python -m jukebox.formats.m3u
__name__    = __main__
__package__ = jukebox.formats
line        = #EXTINF:4:05
where beginners trip
  • Under -m, __name__ is __main__ and __package__ is jukebox. Both true at once, which no plain script can manage.
  • from . import tracks and from .tracks import as_clock are both relative. The first binds a module; the second binds a name out of it.
  • The crash has nothing to do with where the file sits. Move it anywhere, launch it as a script, and __package__ is still "".
  • python -c "from . import jukebox" raises the identical error, for the identical reason. There is no package context at the prompt either.
  • Three dots is not "the top of the project". It is level 3, stripping two names, and it fails outright if __package__ is not that deep.
  • A relative import in a file you also launch directly is the bug everyone writes once. Give that file an absolute import, or launch it with -m.

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, and it never spells the top package's name. So if you rename playlist to setlist tomorrow, every relative import inside keeps working untouched, which makes 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, so 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.

TYPE THIS · save it, then run itimport_styles.py — five spellings of one function, and the is that proves they are one object
# ---------- jukebox/tracks.py ----------
"""tracks.py -- one function, about to be imported five ways."""
print("tracks.py body running")


def as_clock(seconds):
    minutes, secs = divmod(seconds, 60)
    return f"{minutes}:{secs:02d}"


# ---------- jukebox/__init__.py ----------
"""jukebox -- the facade re-exports as_clock."""
from .tracks import as_clock

__all__ = ["as_clock"]


# ---------- import_styles.py ----------
# import_styles.py -- five spellings, ONE function object
import sys

import jukebox                              # 1. the package
import jukebox.tracks                       # 2. the submodule, dotted
from jukebox import tracks                  # 3. the submodule, bare name
from jukebox.tracks import as_clock         # 4. the function itself
from jukebox import as_clock as viafacade   # 5. through __init__.py

spellings = [
    ("jukebox.tracks.as_clock", jukebox.tracks.as_clock),
    ("tracks.as_clock", tracks.as_clock),
    ("as_clock", as_clock),
    ("viafacade", viafacade),
    ("jukebox.as_clock", jukebox.as_clock),
]

for name, fn in spellings:
    print(f"{name:<24} is as_clock -> {fn is as_clock}   {hex(id(fn))}")

print()
print("sys.modules keys  :", [k for k in sys.modules if k.startswith("jukebox")])
print("one module object :", jukebox.tracks is tracks is sys.modules["jukebox.tracks"])
print("where it lives    :", as_clock.__module__)
print("what it computes  :", as_clock(245), viafacade(245), tracks.as_clock(245))
$ python import_styles.py
tracks.py body running
jukebox.tracks.as_clock  is as_clock -> True   0x18927226520
tracks.as_clock          is as_clock -> True   0x18927226520
as_clock                 is as_clock -> True   0x18927226520
viafacade                is as_clock -> True   0x18927226520
jukebox.as_clock         is as_clock -> True   0x18927226520

sys.modules keys  : ['jukebox.tracks', 'jukebox']
one module object : True
where it lives    : jukebox.tracks
what it computes  : 4:05 4:05 4:05
Save the two package files and import_styles.py beside them, then run it. Five import statements, five different spellings, and one line of output tells you the whole story: tracks.py body running printed once. Read the address column next. It is the same hexadecimal number five times, because there is one as_clock function object in this process and every spelling is a name for it. Your number will differ from mine — it is a memory address, and it changes every run — but all five rows will always match each other. That is what is is asserting: not equal values, the same object. Notice which two things are genuinely different here. sys.modules holds two keys, jukebox and jukebox.tracks, because a package and its submodule are two separate module objects. And jukebox.tracks is tracks is True, so line 2 and line 3 did not make two modules — they bound two names to one. The facade adds nothing physical either: jukebox.as_clock came from __init__.py doing from .tracks import as_clock, which is another name for the very same function. Then look at as_clock.__module__. It prints jukebox.tracks no matter which spelling you used to reach it, because the function remembers where it was defined and has no idea how you got hold of it. That is the honest summary of every import in this chapter: import does not copy code, and it does not move it. It runs a module body once and hands out references. Two things to try. Delete the from .tracks import as_clock line in __init__.py, re-run, and watch line 5 fail with an ImportError naming the facade. Then add del jukebox.tracks.as_clock at the end and print as_clock(245) again — your local name still works, because you hold a reference the deletion never touched.
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 command, python -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. 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, with __name__ = "__main__" and __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 takes over: it's called runpy, 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.

SYNTAX · python -m and __main__.py — giving the folder a front doorrunpy imports the package first, then runs __main__.py with the package context still attached
jukebox/ __init__.py -- runs FIRST, every time, before anything else tracks.py __main__.py -- the front door. -m finds it by NAME, not by luck. python -m jukebox -- __init__.py, then __main__.py python -m jukebox play 2 -- the extra words land in sys.argv[1:] python -m jukebox.tracks -- run ONE submodule as __main__ python jukebox/__main__.py -- the broken way: no package context # inside __main__.py, under -m: __name__ == "__main__" -- so the guard fires __package__ == "jukebox" -- so relative imports resolve
-m takes a module nameNot a path. python -m jukebox — never -m jukebox/, never -m jukebox/__main__.py. Dots, not slashes.
__init__.py first-m imports the package before it looks for an entry point, so a broken __init__.py kills the run before line 1 of your program.
__main__.pyPure convention, and the same one the standard library uses. python -m http.server is exactly this file, in that folder.
sys.path[0]Under -m it is the current directory. Under python file.py it is the file's own folder. That single row is the whole difference.
sys.argvIdentical under both forms. sys.argv[0] is the path of the file that ran; your arguments start at index 1, exactly as in a script.
both truths at onceI am the program and I live inside a package. -m is the only way to hold both, and it is why the front door works.
the guard still earns its keep__main__.py runs only under -m, but keep if __name__ == "__main__": in any submodule you want to self-test.
you type
# ---------- jukebox/__init__.py ----------
print("[1] __init__.py  -- the package builds itself first")


# ---------- jukebox/tracks.py ----------
print("[2] tracks.py    -- imported by the entry point")

LIBRARY = ["Blinding Lights", "Titanium", "Levels"]


def as_clock(seconds):
    minutes, secs = divmod(seconds, 60)
    return f"{minutes}:{secs:02d}"


if __name__ == "__main__":
    print("tracks.py self-test:", as_clock(245))


# ---------- jukebox/__main__.py ----------
import sys

from .tracks import LIBRARY, as_clock     # resolves ONLY with a package context

print("[3] __main__.py  -- and only now the program runs")
print("    __name__    =", __name__)
print("    __package__ =", __package__)
print("    sys.argv[1:] =", sys.argv[1:])

if sys.argv[1:2] == ["play"]:
    print("    now playing :", LIBRARY[int(sys.argv[2]) - 1])
else:
    for i, title in enumerate(LIBRARY, start=1):
        print(f"    {i}. {title}")


# ---------- the terminal, standing in myproject/ ----------
$ python -m jukebox
$ python -m jukebox play 2
$ python -m jukebox.tracks
$ python jukebox/__main__.py
you see
$ python -m jukebox
[1] __init__.py  -- the package builds itself first
[2] tracks.py    -- imported by the entry point
[3] __main__.py  -- and only now the program runs
    __name__    = __main__
    __package__ = jukebox
    sys.argv[1:] = []
    1. Blinding Lights
    2. Titanium
    3. Levels

$ python -m jukebox play 2
[1] __init__.py  -- the package builds itself first
[2] tracks.py    -- imported by the entry point
[3] __main__.py  -- and only now the program runs
    __name__    = __main__
    __package__ = jukebox
    sys.argv[1:] = ['play', '2']
    now playing : Titanium

$ python -m jukebox.tracks
[1] __init__.py  -- the package builds itself first
[2] tracks.py    -- imported by the entry point
tracks.py self-test: 4:05

$ python jukebox/__main__.py
Traceback (most recent call last):
  File "C:\tmp\myproject\jukebox\__main__.py", line 4, in <module>
    from .tracks import LIBRARY, as_clock     # resolves ONLY with a package context
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ImportError: attempted relative import with no known parent package
where beginners trip
  • [1] always prints before [3]. -m constructs the package, then runs it — there is no way to skip the first half.
  • python -m jukebox.tracks printed [1] too. Importing a submodule imports its parent first, without exception.
  • The fourth command dies on the relative import, and it dies identically from every directory. Standing somewhere else does not help.
  • -m puts the current directory on sys.path. Run it from inside jukebox/ and Python reports No module named jukebox.
  • No __main__.py in the package gives a very literal error: 'jukebox' is a package and cannot be directly executed.
  • sys.argv[0] under -m is the full path of __main__.py, not the string jukebox. Do not parse a program name out of it.
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. It's "playlist.engine" when imported as a library, and "__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, and stay quiet when I'm a library. The guard has meant that, and only that, all along.

ERROR CLINICyou will meet these — decoded
$ python -m jukebox
jukebox/__init__.py running
C:\Python312\python.exe: No module named jukebox.__main__; 'jukebox' is a package and cannot be directly executed
Two clauses, one semicolon, and each half answers a different question. The first names the search that failed, and notice what it names: a module, not a file. -m takes a dotted module name, so asking for jukebox sent it looking for a submodule spelled exactly __main__ inside that package — which is the fix written out for you, down to the filename. The second half explains why nothing could sensibly be substituted for it. A package's own body is __init__.py, and you can watch it printing directly above the error, so it has already run; running that same file a second time, now under __name__ == "__main__", would put one file into this one process twice under two module objects, which is the exact duplication the cache from section 1 exists to prevent. So read this as a report rather than a fault. Nothing was misspelled, nothing failed to import, and the package built itself perfectly. It simply has no front door yet.
create jukebox/__main__.py — the message already gave you the name; -m finds it by convention, never by configuration
Traceback (most recent call last):
  File "C:\tmp\myproject\run.py", line 1, in <module>
    import jukebox.storage
  File "C:\tmp\myproject\jukebox\storage.py", line 1, in <module>
    from ..settings import DB_PATH     # "up one folder on disk", surely?
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ImportError: attempted relative import beyond top-level package
A different error from the famous one, and the difference is the whole lesson: there is a parent package here, it is just not deep enough. Do section 3's arithmetic on it. Inside jukebox/storage.py, __package__ is 'jukebox' — one segment. Two dots is level 2, which strips level - 1 = 1 name, and stripping one name off a one-segment package leaves nothing to append to. The dots ran out of tree. What makes this one so common is that .. looks exactly like the .. you type in a shell, where it does mean the parent directory — and settings.py really is sitting one folder up, in plain sight. But the dots never walk the disk. They walk __package__, which stops at the outermost package, and the repo root is not a package, so no number of dots can ever reach it. The message is not asking you to add a dot. It is telling you the file you want lives outside the package, and that relative imports have no way out.
decide which side of the wall that file belongs on — move settings.py into jukebox/ and say from .settings import DB_PATH, or leave it outside and let __main__.py read it and pass the value in
Traceback (most recent call last):
  File "C:\tmp\myproject\run.py", line 1, in <module>
    from jukebox import Player
ImportError: cannot import name 'Player' from 'jukebox' (C:\tmp\myproject\jukebox\__init__.py)
The parenthetical is the message, and it is the part everyone skips. That path is not where Player lives; it is the file whose namespace was searched__init__.py, the package's own body. So the traceback is not accusing engine.py of anything. It is saying your facade never put Player on the counter. And from pkg import X did more work than it looks: it checks the package's attributes first, and only if that misses does it try importing pkg.X as a submodule — which is precisely why from jukebox import engine succeeds even though __init__.py never mentions engine. Both routes came up empty here: no attribute Player, and no jukebox/Player.py. One consequence is worth storing away, because it explains a whole genre of confusion. Once anything in the process imports jukebox.engine, that submodule is bound onto the shared package object for good, so jukebox.engine starts resolving in code that never imported it. That is section 1's binding step seen from a distance — and it is why an attribute that works today can vanish the day somebody deletes an import you have never read.
add the line the facade is missing, from .engine import Player in __init__.py, or import the real address with from jukebox.engine import Player — and never depend on an attribute you did not import yourself
NOW WRITE IT YOURSELFone file, three launches — make the crash, name the missing field, then fix it three ways and rank them
First, build the crash. Make myproject/jukebox/ with three files. __init__.py holds only a docstring. tracks.py defines LIBRARY = [("Blinding Lights", 200), ("Titanium", 245), ("Levels", 340)] and as_clock(seconds) returning f"{minutes}:{secs:02d}". report.py does from .tracks import LIBRARY, as_clock, defines summary() returning f"{len(LIBRARY)} tracks, {as_clock(total)}", and ends with a __main__ guard that prints it. From myproject/, run python jukebox/report.py and copy the traceback down. Second, answer three questions before you change one character. Which attribute of the module is wrong, and what is its value in this run? The error says no known parent package — name the parent package it was looking for, and say where the interpreter would have got that string had you imported the file instead. And why does python jukebox/report.py fail identically whether you run it from myproject/ or from anywhere else on the disk? Write the answers out; a fix you cannot explain will be undone by the next person. Third, fix it three ways, then rank them. Fix A: change no code at all and use the launch form that already works. Fix B: copy report.py to report_abs.py, change the import to the absolute from jukebox.tracks import ..., and run it both ways — predict each result before you press Enter, because one of them will surprise you. Fix C: copy it again to report_hack.py, and before the import insert the package's own folder onto sys.path so a bare import tracks resolves; then, in the same file, also import jukebox.tracks and print whether the two are the same object. Say which fix you would ship. One hint and no more: in fix C, append something to tracks.LIBRARY and print the length of both.
show the solution
# ---------- myproject/jukebox/__init__.py ----------
"""jukebox -- a package with one sore spot."""


# ---------- myproject/jukebox/tracks.py ----------
"""tracks.py -- the leaf."""
LIBRARY = [("Blinding Lights", 200), ("Titanium", 245), ("Levels", 340)]


def as_clock(seconds):
    minutes, secs = divmod(seconds, 60)
    return f"{minutes}:{secs:02d}"


# ---------- myproject/jukebox/report.py ----------
from .tracks import LIBRARY, as_clock


def summary():
    total = sum(secs for _title, secs in LIBRARY)
    return f"{len(LIBRARY)} tracks, {as_clock(total)}"


if __name__ == "__main__":
    print(summary())


# ------------- $ python jukebox/report.py -------------
Traceback (most recent call last):
  File "C:\tmp\myproject\jukebox\report.py", line 2, in <module>
    from .tracks import LIBRARY, as_clock
ImportError: attempted relative import with no known parent package


# The three answers.
#
# 1. __package__. Launched as a script, report.py IS the startup module, so the
#    interpreter sets __name__ = "__main__" and __package__ = "". The dot
#    resolver is handed an empty base and has nothing to strip a name from.
#
# 2. It wanted "jukebox". Import it instead -- `import jukebox.report` -- and the
#    loader stamps __package__ = "jukebox" from the dotted name it resolved. The
#    string does not come from the folder on disk; it comes from the import.
#
# 3. Because the script form never consults the working directory. sys.path[0]
#    is the folder of the FILE you launched, which is jukebox/ every time. Where
#    you happen to be standing is not an input.


# ------------- FIX A: change the launch, not the code -------------
$ python -m jukebox.report
3 tracks, 13:05

$ python -c "from jukebox.report import summary; print(summary())"
3 tracks, 13:05


# ------------- FIX B: absolute import. This is the surprise. -------------
# ---------- jukebox/report_abs.py ----------
from jukebox.tracks import LIBRARY, as_clock

if __name__ == "__main__":
    total = sum(secs for _title, secs in LIBRARY)
    print(f"{len(LIBRARY)} tracks, {as_clock(total)}")


$ python jukebox/report_abs.py
Traceback (most recent call last):
  File "C:\tmp\myproject\jukebox\report_abs.py", line 2, in <module>
    from jukebox.tracks import LIBRARY, as_clock
ModuleNotFoundError: No module named 'jukebox'

$ python -m jukebox.report_abs
3 tracks, 13:05

# Absolute imports do NOT rescue the script form. The error simply changes its
# name. sys.path[0] is jukebox/ -- the package's INSIDE -- so the top-level name
# `jukebox` is not on the path at all. Different message, same missing context.


# ------------- FIX C: the sys.path hack, and what it really costs -------------
# ---------- jukebox/report_hack.py ----------
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))   # jukebox/ itself
import tracks                                              # now a TOP-LEVEL name
import jukebox.tracks                                      # and the real one

print("bare import works :", tracks.as_clock(785))
print("same module?      :", tracks is jukebox.tracks)
print("two keys in cache :", [k for k in sys.modules if k.endswith("tracks")])
tracks.LIBRARY.append(("Ghost Track", 1))
print("edit through one  :", len(tracks.LIBRARY), "vs", len(jukebox.tracks.LIBRARY))


$ python -m jukebox.report_hack
bare import works : 13:05
same module?      : False
two keys in cache : ['tracks', 'jukebox.tracks']
edit through one  : 4 vs 3

# There it is. The hack "works" -- and it loaded tracks.py a SECOND time, under a
# second name, into a second module object. Two Track lists, two sets of globals,
# and an append through one that the other never sees. Every class defined in
# there now exists twice, so isinstance() starts returning False for objects that
# obviously match. This is the bug that eats an afternoon.


# The ranking, and why.
#   A  (python -m)          SHIP THIS. No code changed, no name leaked, and the
#                           package keeps exactly one identity in sys.modules.
#   B  (absolute import)    Fine as a style for entry points, but it is not a fix
#                           for the script form -- run it with -m regardless.
#   C  (sys.path.insert)    Never. It converts a private submodule into a global
#                           name and buys you two copies of one module.
#
# The one honest sentence to take away:
#   A relative import needs a package context, and only an IMPORT can create one.
#   Launching a file cannot -- so launch the package with -m instead.
-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, and engine.py needs a type from model.py. Then 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. Correctly, it does not re-run it, because re-running would loop forever, so it hands engine the half-built model object exactly as it stands. But at t4, model has only executed its first line and 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 — 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, and by then 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 understanding gives four fixes, ranked 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," and 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, and engine.py imports model, one direction too. 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 — it's 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.

SYNTAX · the shape of a real repository — where each file goes, and whythe repo root is not a package; the package is one folder inside it, and the tests sit beside it, never in it
jukebox-project/ -- the REPO. no __init__.py. never imported. pyproject.toml -- name, version, dependencies. ch 22 fills this in. README.md jukebox/ -- THE PACKAGE. this folder is what ships. __init__.py -- the facade __main__.py -- the front door: python -m jukebox tracks.py -- the leaf: imports nothing internal storage.py formats/ -- a SUBpackage. a package inside a package. __init__.py json_io.py tests/ -- OUTSIDE the package. imports it like a stranger. __init__.py test_tracks.py # you stand at the repo root for both of these python -m jukebox python -m unittest discover -s tests -t .
the root is not a packageIt holds no __init__.py and is never imported. It is the folder you cd into and the folder git tracks.
one package, one nameEverything importable lives under jukebox/. A second top-level utils.py at the root is the junk drawer growing back.
tests/ outsideTests then import jukebox exactly as a stranger would. Put them inside the package and they ship with it, and stop testing the public surface.
tests/__init__.pyNot decoration. unittest discover refuses a start directory it cannot import, and this one empty file is the whole fix.
a subpackageformats/ is a package like any other — a directory with an __init__.py — reached as jukebox.formats.json_io.
pyproject.tomlWhere the name, the version and the dependencies actually live. It is not read by import; Chapter 22 is where it starts to matter.
where you standsys.path[0] under -m is the current directory, so the repo root is the only place both commands work.
you type
# ---------- tree.py  (at the repo root) ----------
import os

SKIP = {"__pycache__", ".git", ".venv"}

for root, dirs, files in os.walk("."):
    dirs[:] = sorted(d for d in dirs if d not in SKIP)
    depth = root.count(os.sep)
    mark = "  <- package" if "__init__.py" in files else ""
    print("  " * depth + os.path.basename(root or ".") + "/" + mark)
    for name in sorted(files):
        if not name.endswith(".pyc"):
            print("  " * (depth + 1) + name)


# ---------- the terminal ----------
$ python tree.py
$ cd jukebox  &&  python -m jukebox
$ cd ..       &&  python -m jukebox
$ python -m unittest discover -s tests -t .
you see
$ python tree.py
./
  README.md
  pyproject.toml
  tree.py
  jukebox/  <- package
    __init__.py
    __main__.py
    storage.py
    tracks.py
    formats/  <- package
      __init__.py
      json_io.py
  tests/  <- package
    __init__.py
    test_tracks.py

$ cd jukebox  &&  python -m jukebox
C:\...\python.exe: No module named jukebox

$ cd ..       &&  python -m jukebox
  Blinding Lights - The Weeknd (3:20)
  Titanium - David Guetta (4:05)
jukebox 1.0.0

$ python -m unittest discover -s tests -t .
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
where beginners trip
  • Run python -m jukebox from inside jukebox/ and it reports no such module. The parent goes on the path, not the package.
  • A tests/ with no __init__.py makes discovery raise Start directory is not importable. One empty file ends it.
  • Do not let jukebox-project/ and jukebox-project/jukebox/ blur together. Two folders, two jobs, and only one of them is importable.
  • __pycache__ appears inside every package you import. Add the one .gitignore line and stop noticing it.
  • The src/jukebox/ layout is this same picture with one more folder. It stops the repo root shadowing your installed package during tests, and nothing else.
  • tree.py lives at the root, outside the package, because it is a chore script and not part of what ships.
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, from 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, from Section 4. __init__.py re-exports the public trio behind a stable facade and declares __all__, from 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.

TYPE THIS · the chapter's capstone — save it, then run itgrow the jukebox — Chapter 19's three flat files become a package you start with python -m jukebox
# ---------- the layout, after ----------
myproject/
    jukebox/
        __init__.py     # the facade
        tracks.py       # ch 19's tracks.py, unedited
        storage.py      # ch 19's storage.py, ONE import line changed
        __main__.py     # ch 19's jukebox.py, now the front door


# ---------- jukebox/tracks.py ----------
"""tracks.py -- what a track and a playlist ARE. No I/O, no menu."""
from dataclasses import dataclass


@dataclass
class Track:                                  # ch 12's dataclass, unmoved
    title: str
    artist: str
    seconds: int
    plays: int = 0

    def __str__(self):
        minutes, secs = divmod(self.seconds, 60)
        return f"{self.title} - {self.artist} ({minutes}:{secs:02d})"


class Playlist:
    def __init__(self, name, tracks):
        self.name, self.tracks = name, list(tracks)

    def __len__(self):
        return len(self.tracks)

    def __iter__(self):
        return iter(self.tracks)

    def __repr__(self):
        minutes, secs = divmod(sum(t.seconds for t in self), 60)
        return f"Playlist({self.name!r}, {len(self)} tracks, {minutes}:{secs:02d})"

    def play(self, number):
        track = self.tracks[number - 1]
        track.plays += 1
        return track


# ---------- jukebox/storage.py ----------
"""storage.py -- the only file that knows what JSON is. Chapter 18, boxed."""
import json
from pathlib import Path

from .tracks import Track, Playlist          # ch 19 said: from tracks import ...

DB = Path("library.json")


def save(playlist, path=DB):
    payload = {"version": 1, "name": playlist.name,
               "tracks": [vars(t) for t in playlist]}
    with open(path, "w", encoding="utf-8", newline="\n") as f:
        json.dump(payload, f, indent=2)
    return path.stat().st_size


def load(path=DB):
    with open(path, "r", encoding="utf-8") as f:
        data = json.load(f)
    return Playlist(data["name"], [Track(**row) for row in data["tracks"]])


# ---------- jukebox/__init__.py ----------
"""jukebox -- the package. Chapter 19's three flat files, folded under one name."""
from .tracks import Track, Playlist
from .storage import save, load

__all__ = ["Track", "Playlist", "save", "load"]
__version__ = "2.0.0"


# ---------- jukebox/__main__.py ----------
"""__main__.py -- the front door. Chapter 19's jukebox.py, promoted."""
from . import __version__                    # from the facade above it
from .storage import DB, load, save
from .tracks import Track, Playlist


def seed():
    return Playlist("Friday Night", [
        Track("Blinding Lights", "The Weeknd", 200),
        Track("Titanium", "David Guetta", 245),
        Track("Levels", "Avicii", 340),
    ])


def report(playlist):
    print(repr(playlist))
    for i, track in enumerate(playlist, start=1):
        print(f"  {i}. {track}  played {track.plays}x")


def main():
    print(f"jukebox {__version__}  |  __name__ = {__name__}  |  __package__ = {__package__}")
    night = load() if DB.exists() else seed()
    print("loaded from disk:", DB.exists())
    report(night)

    print()
    for number in (2, 1, 2):
        print("  now playing", night.play(number))

    size = save(night)
    print()
    print(f"saved {size} bytes to {DB.name}")


if __name__ == "__main__":
    main()
$ python -m jukebox
jukebox 2.0.0  |  __name__ = __main__  |  __package__ = jukebox
loaded from disk: False
Playlist('Friday Night', 3 tracks, 13:05)
  1. Blinding Lights - The Weeknd (3:20)  played 0x
  2. Titanium - David Guetta (4:05)  played 0x
  3. Levels - Avicii (5:40)  played 0x

  now playing Titanium - David Guetta (4:05)
  now playing Blinding Lights - The Weeknd (3:20)
  now playing Titanium - David Guetta (4:05)

saved 392 bytes to library.json

$ python -m jukebox
jukebox 2.0.0  |  __name__ = __main__  |  __package__ = jukebox
loaded from disk: True
Playlist('Friday Night', 3 tracks, 13:05)
  1. Blinding Lights - The Weeknd (3:20)  played 1x
  2. Titanium - David Guetta (4:05)  played 2x
  3. Levels - Avicii (5:40)  played 0x

  now playing Titanium - David Guetta (4:05)
  now playing Blinding Lights - The Weeknd (3:20)
  now playing Titanium - David Guetta (4:05)

saved 392 bytes to library.json

$ python -c "from jukebox import Track, load; print(load().name, '|', Track('X', 'Y', 61))"
Friday Night | X - Y (1:01)
Make myproject/, make jukebox/ inside it, and save the four files. Then, from myproject/, type python -m jukebox. This is Chapter 19's capstone — tracks.py, storage.py, jukebox.py, three flat files sitting in a folder — grown into a real package, and it is worth being precise about how little actually changed. tracks.py is byte for byte the same file. storage.py changed exactly one line: from tracks import became from .tracks import. jukebox.py was renamed to __main__.py and its two imports grew a dot. Two files are genuinely new, and both are small: an __init__.py that re-exports four names, and nothing else. That is the whole conversion. Now read the first line of output, because it is the thing a flat script cannot print. __name__ is __main__, so main() ran; and __package__ is jukebox, which is why from .storage import DB, load, save resolved at all. Both facts at once — that is runpy doing exactly what section 4 described. Look at the import lines as a shape, too. tracks.py imports nothing internal, storage.py imports only tracks, and __main__.py imports both. Every arrow points down at the leaf, so section 5's circular import is not something you avoided here — it is something the shape forbids. Then run it a second time. loaded from disk flips to True and the play counts come back 1, 2, 0: the first run's plays survived the process that made them, and the JSON on disk is the same JSON Chapter 18 taught you to write. Two things to try. From myproject/, run python jukebox/__main__.py and watch it die on the very first relative import — the front door only opens with -m. Then add a shuffle() method to Playlist and notice that no other file needs an edit, which is the entire point of the leaf.
__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.
NOW WRITE IT YOURSELFadd jukebox/formats/ — a package inside a package, reaching up to its parent with two dots
First, make the folder a package. Start from a jukebox/ holding __init__.py, tracks.py with a Track dataclass (title, artist, seconds, and a __str__ that prints a clock), and a storage.py that owns save and load. Now create jukebox/formats/ with an __init__.py in it, and inside that json_io.py exposing write_tracks(tracks, path) and read_tracks(path). json_io.py needs the Track class, which lives one level up — reach for it with a relative import, and count the dots yourself before you type them. Second, wire it in and move the JSON. Give formats/__init__.py a re-export so the rest of the package can say from .formats import read_tracks, write_tracks without ever naming json_io. Then strip storage.py down: it should import json nowhere, and simply delegate the file shape while keeping the policy — the default path DB. When you are done, exactly one file in the whole project imports json; find it and say so out loud. Third, prove the names. Write __main__.py that saves two tracks, loads them back, prints them, and then prints json_io.__name__ and json_io.__package__. Predict both strings before you run it. Run python -m jukebox from the project root. Finally, answer one question in a sentence: if you renamed the top folder from jukebox to setlist tomorrow, how many import lines inside the package would you have to edit? One hint and no more: from formats/json_io.py, __package__ is "jukebox.formats", and the resolver strips level - 1 names off it.
show the solution
# ---------- the tree, after ----------
$ python tree.py
./
  tree.py
  jukebox/   <- package
    __init__.py
    __main__.py
    storage.py
    tracks.py
    formats/   <- package
      __init__.py
      json_io.py


# ---------- jukebox/tracks.py ----------
"""tracks.py -- the leaf."""
from dataclasses import dataclass


@dataclass
class Track:
    title: str
    artist: str
    seconds: int

    def __str__(self):
        minutes, secs = divmod(self.seconds, 60)
        return f"{self.title} - {self.artist} ({minutes}:{secs:02d})"


# ---------- jukebox/formats/json_io.py ----------
"""json_io.py -- the only file in the project that imports json."""
import json

from ..tracks import Track            # two dots: up to jukebox, then down


def write_tracks(tracks, path):
    rows = [vars(t) for t in tracks]
    with open(path, "w", encoding="utf-8", newline="\n") as f:
        json.dump(rows, f, indent=2)
    return path


def read_tracks(path):
    with open(path, "r", encoding="utf-8") as f:
        return [Track(**row) for row in json.load(f)]


# ---------- jukebox/formats/__init__.py ----------
"""formats -- one module per file shape. A package inside a package."""
from .json_io import read_tracks, write_tracks

__all__ = ["read_tracks", "write_tracks"]


# ---------- jukebox/storage.py ----------
"""storage.py -- delegates the file shape to formats/, keeps the policy."""
from .formats import read_tracks, write_tracks

DB = "library.json"


def save(tracks, path=DB):
    return write_tracks(tracks, path)


def load(path=DB):
    return read_tracks(path)


# ---------- jukebox/__init__.py ----------
"""jukebox -- now with a formats subpackage."""
from .tracks import Track
from .storage import load, save

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


# ---------- jukebox/__main__.py ----------
"""__main__.py -- the front door."""
from .storage import load, save
from .tracks import Track

save([Track("Blinding Lights", "The Weeknd", 200),
      Track("Titanium", "David Guetta", 245)])

for track in load():
    print(" ", track)

from .formats import json_io
print("json_io.__package__ =", json_io.__package__)
print("json_io.__name__    =", json_io.__name__)


# ------------- $ python -m jukebox -------------
  Blinding Lights - The Weeknd (3:20)
  Titanium - David Guetta (4:05)
json_io.__package__ = jukebox.formats
json_io.__name__    = jukebox.formats.json_io


# ------------- the dot arithmetic, checked -------------
# In jukebox/formats/json_io.py, __package__ is "jukebox.formats".
#   from ..tracks import Track   ->  level = 2
#                                    strip level-1 = 1 name  ->  "jukebox"
#                                    append "tracks"         ->  "jukebox.tracks"
# One dot would have resolved to "jukebox.formats.tracks", which does not exist.


$ python -c "import jukebox.formats.json_io as j; print(j.__name__, j.__package__)"
jukebox.formats.json_io | jukebox.formats


# The rename question:
#   ZERO import lines inside the package. Every one of them is relative, so not
#   one of them spells `jukebox`. Rename the folder to setlist, run
#   `python -m setlist`, and it works untouched. Only code OUTSIDE the package --
#   which said `from jukebox import ...` -- has to change.
#
# And exactly one file imports json: jukebox/formats/json_io.py. Adding an
# m3u.py beside it later needs no edit to storage.py, because storage.py asks
# formats/ for a shape and never for a format.

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.

NOW WRITE IT YOURSELFdesign the tree — a habit tracker described in one paragraph, laid out as a repository you can defend
First, read the brief and draw the tree on paper. Here is the app, described the way a teammate would describe it. A habit tracker. It keeps a list of habits, each with a name and the dates it was done. It reads and writes them as JSON. It computes the current streak for each habit — consecutive days ending today, or ending yesterday if today is not marked yet. Running it prints every habit with its streak, and add "walk the dog" appends a new one. It must run as one command, and the streak rule must be testable without touching the disk. Before writing any code, sketch the repository: which folder is the package, which files live in it, which one is the leaf, and where the tests go. Draw an arrow from each module to the modules it imports, and check that they all point the same way. Second, write it. Four modules and an entry point is enough. Keep the streak rule in a file that imports no I/O of any kind, and keep every open() and every json call in one file. Pin "today" to a fixed date in the entry point rather than calling date.today(), so your output is reproducible and so is mine. Add tests/ beside the package with three cases: an unbroken chain, a chain that was broken and reset, and a habit never done. Third, run both commands and defend two decisions. python -m habits, then python -m unittest discover -s tests -t ., both from the repo root. Then write two sentences. Why does the streak function take today as an argument instead of reading the clock? And why is tests/ outside the package rather than inside it? One hint and no more: unittest discover refuses a start directory it cannot import.
show the solution
# ---------- the tree ----------
$ python tree.py
./
  README.md
  pyproject.toml
  tree.py
  habits/   <- package
    __init__.py
    __main__.py
    model.py
    storage.py
    streaks.py
  tests/   <- package
    __init__.py
    test_streaks.py

# The arrows, all pointing down at the leaf:
#
#            __main__.py            (the front door: wires, owns no logic)
#            /    |     \
#     storage  streaks  model
#            \    |     /
#              model.py             (the LEAF: imports nothing internal)
#
# __init__.py sits above all of it as the facade. No module imports __main__,
# and no module imports __init__ except __main__ (for the version string), so
# there is no cycle available to write.


# ---------- habits/model.py ----------
"""model.py -- LEAF. What a habit IS. Imports nothing internal."""
from dataclasses import dataclass, field


@dataclass
class Habit:
    name: str
    done: list = field(default_factory=list)     # ISO dates, most recent last


# ---------- habits/streaks.py ----------
"""streaks.py -- pure logic. One arrow down to model, no I/O at all."""
from datetime import date, timedelta

from .model import Habit


def streak(habit, today):
    """Consecutive days ending today (or yesterday) -- 0 if the chain is broken."""
    days = {date.fromisoformat(d) for d in habit.done}
    cursor = today if today in days else today - timedelta(days=1)
    count = 0
    while cursor in days:
        count += 1
        cursor -= timedelta(days=1)
    return count


# ---------- habits/storage.py ----------
"""storage.py -- the only file that knows what JSON is. One arrow down to model."""
import json
from pathlib import Path

from .model import Habit

DB = Path("habits.json")


def load(path=DB):
    if not path.exists():
        return []
    with open(path, "r", encoding="utf-8") as f:
        return [Habit(**row) for row in json.load(f)]


def save(habits, path=DB):
    with open(path, "w", encoding="utf-8", newline="\n") as f:
        json.dump([vars(h) for h in habits], f, indent=2)
    return path


# ---------- habits/__init__.py ----------
"""habits -- track a streak. Run it with `python -m habits`."""
from .model import Habit
from .storage import load, save
from .streaks import streak

__all__ = ["Habit", "load", "save", "streak"]
__version__ = "1.0.0"


# ---------- habits/__main__.py ----------
"""__main__.py -- the front door. Wires storage + streaks; owns no logic."""
import sys
from datetime import date, timedelta

from .model import Habit
from .storage import load, save
from .streaks import streak

TODAY = date(2026, 8, 16)                    # fixed so the output is reproducible


def seed():
    days = [(TODAY - timedelta(days=n)).isoformat() for n in (3, 2, 1, 0)]
    return [Habit("read 20 pages", days), Habit("run 5k", days[-2:])]


def main(argv):
    habits = load() or seed()
    if argv[:1] == ["add"]:
        habits.append(Habit(argv[1], [TODAY.isoformat()]))
    for habit in habits:
        print(f"  {habit.name:<16} {streak(habit, TODAY)} day streak")
    save(habits)


if __name__ == "__main__":
    main(sys.argv[1:])


# ---------- tests/__init__.py ----------
"""tests -- a package, so `unittest discover` can import it."""


# ---------- tests/test_streaks.py ----------
"""The logic is testable because streaks.py never touches the disk."""
import unittest
from datetime import date

from habits import Habit, streak


class StreakTests(unittest.TestCase):
    TODAY = date(2026, 8, 16)

    def test_unbroken_chain(self):
        h = Habit("read", ["2026-08-14", "2026-08-15", "2026-08-16"])
        self.assertEqual(streak(h, self.TODAY), 3)

    def test_broken_chain_resets(self):
        h = Habit("read", ["2026-08-10", "2026-08-16"])
        self.assertEqual(streak(h, self.TODAY), 1)

    def test_never_done(self):
        self.assertEqual(streak(Habit("run"), self.TODAY), 0)


# ------------- both commands, from the repo root -------------
$ python -m habits
  read 20 pages    4 day streak
  run 5k           2 day streak

$ python -m habits add "walk the dog"
  read 20 pages    4 day streak
  run 5k           2 day streak
  walk the dog     1 day streak

$ python -m unittest discover -s tests -t .
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK


# The two defences.
#
# 1. streak() takes `today` because a function that reads the clock cannot be
#    tested -- its answer changes at midnight and you cannot ask it about a
#    Tuesday in 2026. Passing the date makes the rule a pure function of its
#    inputs, which is why all three tests are three lines each. The clock is
#    read in exactly one place, __main__.py, where the program meets the world.
#
# 2. tests/ is outside habits/ because it must import the package the way a
#    stranger does -- `from habits import Habit, streak`, through the facade.
#    Put it inside and it ships to users, it can reach private names by
#    accident, and it stops testing the surface you actually promised.
#
# The layout rule, in one line:
#   Every arrow points at model.py, and only __main__.py is allowed to know
#   about more than one layer at a time.
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. →

SAY IT BACKthe chapter in five breaths
  1. A package is a directory the import system treats as one name, and the single thing that makes it one is __path__ — an ordinary list holding that folder, which is the map every sub-import consults — so importing the package runs __init__.py and stops, and jukebox.tracks is an AttributeError until something actually imports it and the machinery binds the child onto its parent.
  2. __init__.py is not a marker file, it is the package's module body: real code executed once, top to bottom, with the package's own namespace as its globals — which means empty is a complete choice, the names it binds are the public surface, and every re-export you add runs a whole submodule at package-import time, so a generous facade and a fast import pull against each other.
  3. __all__ and the leading underscore each govern exactly one thing, which is what from pkg import * copies out — neither hides anything, attribute access reaches every name you ask for, and Python's "private" is a fence rather than a wall, a promise between people that the interpreter takes no part in enforcing.
  4. The dots are arithmetic on __package__, and only an import can create that string — one dot is level 1 and strips nothing, each extra dot strips one more name, and the string itself is stamped on by the loader from the dotted name it resolved — which is why python -m pkg can hold both truths at once, __name__ == "__main__" and __package__ == "pkg", and why python pkg/file.py can hold neither.
  5. A cycle is not fatal by itself; what is fatal is reaching for a name while the other module is still executing — the half-built object was already in sys.modules, so from X import name demands something not yet defined while import X then X.name waits until call time — and pointing every arrow down at a leaf makes the whole question structurally impossible to ask.
You already owned the pieces: chapter 3 gave you the binding, so jukebox.tracks reads as one more name in one more namespace rather than a path on a disk; chapter 7 gave you the hash table that a module's __dict__ and sys.modules both literally are, which is why a package's public surface is a lookup and not a manifest; chapter 12 gave you the dataclass that model.py holds as its leaf — the stable contract storage produces and engine consumes; chapter 13 gave you the honest reading of a traceback, which is the only reason partially initialized module lands as information rather than alarm; chapter 18 gave you the JSON that storage.py boxes up so nothing else in the package has to know a file format exists; chapter 19 gave you the four verbs — find, compile, exec, cache — and the three flat files this chapter folds under one name; and chapter 20 gave you the one line everything here turns on, sys.modules[name] = mod executed before a single statement of the body runs, which is at once why __init__.py executes exactly once and why a circular import hands you a half-built module instead of looping forever. Chapter 21 added no new machinery at all — not one new verb, not one new call. What it added is a direction. The same import statements, arranged so that every arrow points at a leaf and only the front door knows about more than one layer, stop being a folder of files and become a program you can change one piece at a time.
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
designed & developed by Ajai Raj
Python, from the metal up · a product of iolinked