Yesterday you finished __name__ == '__main__' — the warehouse that acts as a showroom when someone walks in directly, but as a fulfillment center when orders come in through the loading dock. Today we go one level up. What's your current project structure looking like?
inventory.py, reporting.py, and suppliers.py all sitting in the same folder. Every time I open one I lose track of which file does what. It's only three files and already confusing.
Three files in a flat folder is where most projects start. It stops working when you hit eight. At some point you stop organizing by file and start organizing by department. That's a package.
A package is just a folder of Python files?
Nearly. A folder with one special addition: an __init__.py file. That file — even when empty — tells Python "this directory is a package, not just a folder." Without it, Python won't let you import from the directory.
So __init__.py is what separates a pile of files from an importable namespace.
Think about Diane's physical warehouse. When you walk in, you see departments: Electronics in aisle 3, Clothing in aisle 7, Safety Equipment in aisle 12. You don't walk to the loading dock and ask for "a product" — you say "Electronics, item SKU-1001." The department tells you where to look:
src/
warehouse/
__init__.py
inventory/
__init__.py
products.py
categories.py
reporting/
__init__.py
daily.py
weekly.pyA package is a department. from warehouse.inventory.products import get_product is like saying "Electronics, aisle 3, shelf B — get me that item."
And each dot is a folder level. warehouse.inventory.products maps to src/warehouse/inventory/products.py. Each intermediate directory must have its own __init__.py. That's the tree: branches are packages, leaf is the module file.
And each branch needs __init__.py... so for warehouse.inventory.products I need warehouse/__init__.py and warehouse/inventory/__init__.py. The leaf products.py is just a file — no __init__.py needed.
Right. Missing one __init__.py in the chain gives you ModuleNotFoundError. The error message doesn't always make the cause obvious — it just says the module can't be found, and you're left wondering if you spelled something wrong.
I hit exactly that in Track 1. I had a utils folder and kept getting ModuleNotFoundError. It was the missing __init__.py. I spent an hour on it.
Every Python developer has that story. Once you know to look for it, it takes ten seconds to fix. The __init__.py — it can be empty, or it can do something useful. The common pattern is using it to flatten your public interface:
# warehouse/inventory/__init__.py
from warehouse.inventory.products import get_product, update_stock
from warehouse.inventory.categories import get_categoriesSo callers can write from warehouse.inventory import get_product instead of from warehouse.inventory.products import get_product. The internal file structure becomes an implementation detail — the __init__.py is the package's front desk.
You're thinking like a package designer. Now — the practical problem today builds on. Given a dotted import path, where does that file actually live on disk? And which __init__.py files need to exist?
"warehouse.inventory.products" maps to src/warehouse/inventory/products.py. The __init__.py files go in src/warehouse/ and src/warehouse/inventory/. Not in the leaf — that's just a .py file.
Work out the algorithm from that. What are the steps?
Split on dots. The last segment is the module file — add .py. Each preceding segment is a directory. Incrementally build the package directories: src/warehouse, then src/warehouse/inventory. Each one gets __init__.py appended for the init files list.
That's the whole algorithm. And notice: the leaf never appears in the package dirs list. Only the branches — the directories — get __init__.py. Leaves are files.
Two metaphors — the warehouse department and the tree. You're not apologizing, are you.
I need both and I'm not apologizing. Let me sketch the function shape:
def resolve_package_path(dotted_path: str, base_dir: str = "src") -> dict:
parts = dotted_path.split(".")
# ["warehouse", "inventory", "products"]
file_path = base_dir + "/" + "/".join(parts) + ".py"
# "src/warehouse/inventory/products.py"
package_dirs = []
for i in range(1, len(parts)): # skip the last part — that's the module file
package_dirs.append(base_dir + "/" + "/".join(parts[:i]))
# ["src/warehouse", "src/warehouse/inventory"]
init_files = [d + "/__init__.py" for d in package_dirs]
return {
"import_path": dotted_path,
"file_path": file_path,
"package_dirs": package_dirs,
"init_files": init_files,
}range(1, len(parts)) gives indices 1 and 2 for three parts. parts[:1] is ["warehouse"], parts[:2] is ["warehouse", "inventory"]. Index 3 — len(parts) — is excluded, so products never becomes a package dir.
Trace the single-level edge case: resolve_package_path("utils", "src").
parts = ["utils"]. file_path = "src/utils.py". range(1, 1) is empty — loop never runs. package_dirs = []. init_files = []. Correct — a top-level module has no parent packages needing __init__.py.
No special case, no if len(parts) == 1 guard. The math just works out for the boundary condition.
The edge case handles itself. I love when that happens. So after today I can look at any dotted import — django.db.models, warehouse.reporting.weekly — and mentally walk the directory tree. I know where the file lives and which __init__.py files have to exist.
Tomorrow: virtual environments. You've been using import os and import json without thinking about where those live. Tomorrow you find out — and you'll find out why every serious Python project has a requirements.txt and a folder called .venv that you should never commit to Git.
.venv — that's "separate warehouses for separate clients," right? Different projects, different dependencies, no overlap.
You remembered an analogy I haven't taught yet. Either you read ahead or you have very good pattern recognition.
You mentioned it on Day 23 when I asked why you had a .venv in your project. I filed it away.
A Python package is a directory containing an __init__.py file. Without __init__.py, Python treats the directory as a plain folder and cannot import from it. Sub-packages are directories with their own __init__.py nested inside a parent package.
src/
warehouse/
__init__.py ← marks warehouse as a package
inventory/
__init__.py ← marks inventory as a sub-package
products.py ← leaf module — no __init__.py
categories.py
reporting/
__init__.py
daily.pyImport syntax maps to the file system:
from warehouse.inventory.products import get_product
# warehouse/__init__.py must exist
# warehouse/inventory/__init__.py must exist
# warehouse/inventory/products.py must exist__init__.py as a public API: A non-empty __init__.py can re-export names to flatten the interface:
# warehouse/inventory/__init__.py
from warehouse.inventory.products import get_product, update_stock
# Callers can now write:
from warehouse.inventory import get_product # instead of from .products import ...This makes internal file structure an implementation detail — changing which file defines get_product doesn't break callers as long as the __init__.py re-export stays in place.
Algorithm for dotted path → file system:
. to get partsfile_path = base_dir + "/" + "/".join(parts) + ".py"package_dirs = [base_dir + "/" + "/".join(parts[:i]) for i in range(1, len(parts))]init_files = [d + "/__init__.py" for d in package_dirs]The leaf (last segment) is always a file — it never becomes a package directory.
Pitfall 1: Missing __init__.py at any level. A missing __init__.py in any intermediate directory results in ModuleNotFoundError. The error message doesn't always indicate the cause. If you get unexpected import errors, check every directory in the chain for __init__.py.
Pitfall 2: Circular imports through __init__.py. If warehouse/__init__.py imports from warehouse/inventory/__init__.py which imports from warehouse/__init__.py, both fail to fully initialize. Keep __init__.py imports to re-exports only — don't use them for logic.
Pitfall 3: Shadowing standard library packages. A directory named json/ with __init__.py shadows the standard library json package. Use unique, project-specific package names.
Python 3.3+ namespace packages: Directories without __init__.py can be imported in specific scenarios (PEP 420). This allows splitting a single package across multiple directories. For all standard project use cases, explicit __init__.py is still the correct convention.
__all__ in __init__.py: Setting __all__ = ["get_product", "update_stock"] controls what from warehouse.inventory import * exports. Without __all__, import * exports everything not prefixed with _.
Package-relative imports: Inside a package, from . import products (single dot) imports from the current package. from .. import utils (double dot) imports from the parent package. Relative imports prevent shadowing and make packages portable.
Sign up to write and run code in this lesson.
Yesterday you finished __name__ == '__main__' — the warehouse that acts as a showroom when someone walks in directly, but as a fulfillment center when orders come in through the loading dock. Today we go one level up. What's your current project structure looking like?
inventory.py, reporting.py, and suppliers.py all sitting in the same folder. Every time I open one I lose track of which file does what. It's only three files and already confusing.
Three files in a flat folder is where most projects start. It stops working when you hit eight. At some point you stop organizing by file and start organizing by department. That's a package.
A package is just a folder of Python files?
Nearly. A folder with one special addition: an __init__.py file. That file — even when empty — tells Python "this directory is a package, not just a folder." Without it, Python won't let you import from the directory.
So __init__.py is what separates a pile of files from an importable namespace.
Think about Diane's physical warehouse. When you walk in, you see departments: Electronics in aisle 3, Clothing in aisle 7, Safety Equipment in aisle 12. You don't walk to the loading dock and ask for "a product" — you say "Electronics, item SKU-1001." The department tells you where to look:
src/
warehouse/
__init__.py
inventory/
__init__.py
products.py
categories.py
reporting/
__init__.py
daily.py
weekly.pyA package is a department. from warehouse.inventory.products import get_product is like saying "Electronics, aisle 3, shelf B — get me that item."
And each dot is a folder level. warehouse.inventory.products maps to src/warehouse/inventory/products.py. Each intermediate directory must have its own __init__.py. That's the tree: branches are packages, leaf is the module file.
And each branch needs __init__.py... so for warehouse.inventory.products I need warehouse/__init__.py and warehouse/inventory/__init__.py. The leaf products.py is just a file — no __init__.py needed.
Right. Missing one __init__.py in the chain gives you ModuleNotFoundError. The error message doesn't always make the cause obvious — it just says the module can't be found, and you're left wondering if you spelled something wrong.
I hit exactly that in Track 1. I had a utils folder and kept getting ModuleNotFoundError. It was the missing __init__.py. I spent an hour on it.
Every Python developer has that story. Once you know to look for it, it takes ten seconds to fix. The __init__.py — it can be empty, or it can do something useful. The common pattern is using it to flatten your public interface:
# warehouse/inventory/__init__.py
from warehouse.inventory.products import get_product, update_stock
from warehouse.inventory.categories import get_categoriesSo callers can write from warehouse.inventory import get_product instead of from warehouse.inventory.products import get_product. The internal file structure becomes an implementation detail — the __init__.py is the package's front desk.
You're thinking like a package designer. Now — the practical problem today builds on. Given a dotted import path, where does that file actually live on disk? And which __init__.py files need to exist?
"warehouse.inventory.products" maps to src/warehouse/inventory/products.py. The __init__.py files go in src/warehouse/ and src/warehouse/inventory/. Not in the leaf — that's just a .py file.
Work out the algorithm from that. What are the steps?
Split on dots. The last segment is the module file — add .py. Each preceding segment is a directory. Incrementally build the package directories: src/warehouse, then src/warehouse/inventory. Each one gets __init__.py appended for the init files list.
That's the whole algorithm. And notice: the leaf never appears in the package dirs list. Only the branches — the directories — get __init__.py. Leaves are files.
Two metaphors — the warehouse department and the tree. You're not apologizing, are you.
I need both and I'm not apologizing. Let me sketch the function shape:
def resolve_package_path(dotted_path: str, base_dir: str = "src") -> dict:
parts = dotted_path.split(".")
# ["warehouse", "inventory", "products"]
file_path = base_dir + "/" + "/".join(parts) + ".py"
# "src/warehouse/inventory/products.py"
package_dirs = []
for i in range(1, len(parts)): # skip the last part — that's the module file
package_dirs.append(base_dir + "/" + "/".join(parts[:i]))
# ["src/warehouse", "src/warehouse/inventory"]
init_files = [d + "/__init__.py" for d in package_dirs]
return {
"import_path": dotted_path,
"file_path": file_path,
"package_dirs": package_dirs,
"init_files": init_files,
}range(1, len(parts)) gives indices 1 and 2 for three parts. parts[:1] is ["warehouse"], parts[:2] is ["warehouse", "inventory"]. Index 3 — len(parts) — is excluded, so products never becomes a package dir.
Trace the single-level edge case: resolve_package_path("utils", "src").
parts = ["utils"]. file_path = "src/utils.py". range(1, 1) is empty — loop never runs. package_dirs = []. init_files = []. Correct — a top-level module has no parent packages needing __init__.py.
No special case, no if len(parts) == 1 guard. The math just works out for the boundary condition.
The edge case handles itself. I love when that happens. So after today I can look at any dotted import — django.db.models, warehouse.reporting.weekly — and mentally walk the directory tree. I know where the file lives and which __init__.py files have to exist.
Tomorrow: virtual environments. You've been using import os and import json without thinking about where those live. Tomorrow you find out — and you'll find out why every serious Python project has a requirements.txt and a folder called .venv that you should never commit to Git.
.venv — that's "separate warehouses for separate clients," right? Different projects, different dependencies, no overlap.
You remembered an analogy I haven't taught yet. Either you read ahead or you have very good pattern recognition.
You mentioned it on Day 23 when I asked why you had a .venv in your project. I filed it away.
A Python package is a directory containing an __init__.py file. Without __init__.py, Python treats the directory as a plain folder and cannot import from it. Sub-packages are directories with their own __init__.py nested inside a parent package.
src/
warehouse/
__init__.py ← marks warehouse as a package
inventory/
__init__.py ← marks inventory as a sub-package
products.py ← leaf module — no __init__.py
categories.py
reporting/
__init__.py
daily.pyImport syntax maps to the file system:
from warehouse.inventory.products import get_product
# warehouse/__init__.py must exist
# warehouse/inventory/__init__.py must exist
# warehouse/inventory/products.py must exist__init__.py as a public API: A non-empty __init__.py can re-export names to flatten the interface:
# warehouse/inventory/__init__.py
from warehouse.inventory.products import get_product, update_stock
# Callers can now write:
from warehouse.inventory import get_product # instead of from .products import ...This makes internal file structure an implementation detail — changing which file defines get_product doesn't break callers as long as the __init__.py re-export stays in place.
Algorithm for dotted path → file system:
. to get partsfile_path = base_dir + "/" + "/".join(parts) + ".py"package_dirs = [base_dir + "/" + "/".join(parts[:i]) for i in range(1, len(parts))]init_files = [d + "/__init__.py" for d in package_dirs]The leaf (last segment) is always a file — it never becomes a package directory.
Pitfall 1: Missing __init__.py at any level. A missing __init__.py in any intermediate directory results in ModuleNotFoundError. The error message doesn't always indicate the cause. If you get unexpected import errors, check every directory in the chain for __init__.py.
Pitfall 2: Circular imports through __init__.py. If warehouse/__init__.py imports from warehouse/inventory/__init__.py which imports from warehouse/__init__.py, both fail to fully initialize. Keep __init__.py imports to re-exports only — don't use them for logic.
Pitfall 3: Shadowing standard library packages. A directory named json/ with __init__.py shadows the standard library json package. Use unique, project-specific package names.
Python 3.3+ namespace packages: Directories without __init__.py can be imported in specific scenarios (PEP 420). This allows splitting a single package across multiple directories. For all standard project use cases, explicit __init__.py is still the correct convention.
__all__ in __init__.py: Setting __all__ = ["get_product", "update_stock"] controls what from warehouse.inventory import * exports. Without __all__, import * exports everything not prefixed with _.
Package-relative imports: Inside a package, from . import products (single dot) imports from the current package. from .. import utils (double dot) imports from the parent package. Relative imports prevent shadowing and make packages portable.