Every Python file you've written so far has been a single script. When the file gets to 500+ lines you split it. The split unit is a module: one .py file is one module.
# models.py
def double(n):
return n * 2
# main.py
from models import double
print(double(5)) # 10And import models vs from models import double — what's the difference?
Both work. import models brings the module in as a single name; you call models.double(5). from models import double brings the function in directly; you call double(5). The latter is shorter at the call site but pollutes the namespace more (every from x import * is a footgun).
And a package — that's a directory of modules?
Right. A package is a directory containing an __init__.py file (which can be empty) plus one or more .py modules. The __init__.py runs when you import the package — typically just to expose its main symbols:
mini/
__init__.py
models.py
helpers.py
# mini/__init__.py
from .models import Item # exposes mini.Item at the top level
# user code
from mini import ItemSo packages are just folders with that special file?
That's the gist. In Python 3.3+ you can also have namespace packages without an __init__.py, but for a small library the explicit __init__.py is clearer.
Today's exercise — we don't actually have a real filesystem in the browser. How do we test this?
We use sys.modules — Python's in-memory cache of imported modules. We register a fake module by hand, then import it. Same mechanic as a real from x import y — just bypassing the file lookup step.
A Python module is just a .py file. The module's name is the filename without .py.
# models.py
def double(n):
return n * 2
class Item:
def __init__(self, name, value):
self.name = name
self.value = value# 1. Whole module
import models
result = models.double(5)
# 2. Specific names
from models import double, Item
result = double(5)
# 3. Aliased
import models as m
result = m.double(5)from x import * — almost neverfrom models import * # imports every public name — bad in real codeReadability suffers — you can't tell where double came from. Use it only in interactive sessions, never in production modules.
__init__.pymini/ ← package directory
__init__.py ← marks it as a package; runs on import
models.py ← submodule
helpers.py ← submodule
__init__.py runs on importWhen you import mini (or from mini import x), Python runs mini/__init__.py first. Common content:
# mini/__init__.py
from .models import Item, double # "." = same package
from .helpers import format_item
__all__ = ["Item", "double", "format_item"] # what `from mini import *` exposesThis lets users from mini import Item instead of from mini.models import Item — flatter import surface.
# mini/models.py
from .helpers import format_item # relative — same package
# vs
from mini.helpers import format_item # absolute — by full pathWithin a package, relative (from .x) is conventional — it makes refactoring (renaming the package) easier.
A package can contain another package:
mini/
__init__.py
models.py
tests/
__init__.py
test_models.py
from mini.tests.test_models import TestItem works the same way — chain the dots.
In a real project the module lives on disk. In Pyodide we don't have a writable filesystem here. The standard trick:
import sys, types
m = types.ModuleType("models")
m.double = lambda n: n * 2
sys.modules["models"] = m
from models import double
print(double(5)) # 10We build a ModuleType object, attach functions/classes to it, and register it in sys.modules. After that, import models and from models import x work normally — because that's exactly what they look up.
Real-world: skip the types.ModuleType step, just write models.py. The mental model is the same.
Every Python file you've written so far has been a single script. When the file gets to 500+ lines you split it. The split unit is a module: one .py file is one module.
# models.py
def double(n):
return n * 2
# main.py
from models import double
print(double(5)) # 10And import models vs from models import double — what's the difference?
Both work. import models brings the module in as a single name; you call models.double(5). from models import double brings the function in directly; you call double(5). The latter is shorter at the call site but pollutes the namespace more (every from x import * is a footgun).
And a package — that's a directory of modules?
Right. A package is a directory containing an __init__.py file (which can be empty) plus one or more .py modules. The __init__.py runs when you import the package — typically just to expose its main symbols:
mini/
__init__.py
models.py
helpers.py
# mini/__init__.py
from .models import Item # exposes mini.Item at the top level
# user code
from mini import ItemSo packages are just folders with that special file?
That's the gist. In Python 3.3+ you can also have namespace packages without an __init__.py, but for a small library the explicit __init__.py is clearer.
Today's exercise — we don't actually have a real filesystem in the browser. How do we test this?
We use sys.modules — Python's in-memory cache of imported modules. We register a fake module by hand, then import it. Same mechanic as a real from x import y — just bypassing the file lookup step.
A Python module is just a .py file. The module's name is the filename without .py.
# models.py
def double(n):
return n * 2
class Item:
def __init__(self, name, value):
self.name = name
self.value = value# 1. Whole module
import models
result = models.double(5)
# 2. Specific names
from models import double, Item
result = double(5)
# 3. Aliased
import models as m
result = m.double(5)from x import * — almost neverfrom models import * # imports every public name — bad in real codeReadability suffers — you can't tell where double came from. Use it only in interactive sessions, never in production modules.
__init__.pymini/ ← package directory
__init__.py ← marks it as a package; runs on import
models.py ← submodule
helpers.py ← submodule
__init__.py runs on importWhen you import mini (or from mini import x), Python runs mini/__init__.py first. Common content:
# mini/__init__.py
from .models import Item, double # "." = same package
from .helpers import format_item
__all__ = ["Item", "double", "format_item"] # what `from mini import *` exposesThis lets users from mini import Item instead of from mini.models import Item — flatter import surface.
# mini/models.py
from .helpers import format_item # relative — same package
# vs
from mini.helpers import format_item # absolute — by full pathWithin a package, relative (from .x) is conventional — it makes refactoring (renaming the package) easier.
A package can contain another package:
mini/
__init__.py
models.py
tests/
__init__.py
test_models.py
from mini.tests.test_models import TestItem works the same way — chain the dots.
In a real project the module lives on disk. In Pyodide we don't have a writable filesystem here. The standard trick:
import sys, types
m = types.ModuleType("models")
m.double = lambda n: n * 2
sys.modules["models"] = m
from models import double
print(double(5)) # 10We build a ModuleType object, attach functions/classes to it, and register it in sys.modules. After that, import models and from models import x work normally — because that's exactly what they look up.
Real-world: skip the types.ModuleType step, just write models.py. The mental model is the same.
Create a free account to get started. Paid plans unlock all tracks.