Packages and init
Packages and __init__
A package is a directory of modules with a way to be imported as desk.tickets. The boring default is a folder named desk/ with an __init__.py and a few sibling .py files. Relative imports (from .tickets import Ticket) keep the package movable. A namespace package is a directory without __init__.py; skip it unless you are splitting one package across locations.
Mental model
import desk.tickets means: find a directory desk on sys.path, treat it as a package, load desk/tickets.py as the submodule desk.tickets.
__init__.py runs when the package itself is first imported (import desk). Put re-exports there if the public names are few. Leave it empty if callers should write desk.tickets.
A leading . in from .tickets import Ticket means “the package I belong to.” Two dots (..) means the parent package. Relative imports only work when the file is loaded as part of a package, not when you run uv run python desk/labels.py.
A namespace package (PEP 420) is a directory with no __init__.py. Python can merge several such directories of the same name on sys.path. Ordinary application code does not need that.
Worked examples
Create a folder desk next to the program you will run. Save the package files inside desk/. Save show_desk.py beside desk/, not inside it. Run from that parent folder.
Case 1: A directory with __init__.py
Save as desk/tickets.py:
# desk/tickets.py
class Ticket:
def __init__(self, id, table):
self.id = id
self.table = tableSave as desk/__init__.py. Importing desk re-exports Ticket.
# desk/__init__.py
from .tickets import Ticket
__all__ = ["Ticket"]Save as show_desk.py:
# show_desk.py
import desk
def main():
t = desk.Ticket(7, 12)
print(t.id, t.table)
print(desk.__all__)
if __name__ == "__main__":
main()Run (from the parent of desk/):
uv run python show_desk.pyOutput:
7 12
['Ticket']
__all__ lists the public names for from desk import *. You still will not star-import. The list is documentation.
Case 2: Relative imports between siblings
Save as desk/labels.py. The . is required: tickets is a sibling, not a top-level module.
# desk/labels.py
from .tickets import Ticket
def label(ticket):
return f"ticket {ticket.id} → table {ticket.table}"Save as show_label.py next to desk/:
# show_label.py
from desk.labels import label
from desk.tickets import Ticket
def main():
print(label(Ticket(7, 12)))
if __name__ == "__main__":
main()Run:
uv run python show_label.pyOutput:
ticket 7 → table 12
Absolute from desk.tickets import Ticket also works inside the package. Relative imports stay correct if you later rename the top folder’s path but keep the package structure. Pick one style per package and stick to it. Relative is the usual choice among siblings.
Case 3: A namespace package, briefly
Save as tools/clock.py. There is no tools/__init__.py.
# tools/clock.py
def now():
return "09:00"Save as show_tools.py next to tools/:
# show_tools.py
import tools.clock
import tools
def main():
print(tools.clock.now())
print(tools.__file__)
if __name__ == "__main__":
main()Run:
uv run python show_tools.pyOutput:
09:00
None
tools imported. __file__ is None because a namespace package is a list of locations, not one source file. For application code, add __init__.py anyway. You want a regular package with a single directory and a place to put re-exports.
The trap
Relative imports fail when the file is run as a script. Python then has no package context.
Keep desk/labels.py as in Case 2. Run it directly:
uv run python desk/labels.pyOutput (the process exits non-zero):
Traceback (most recent call last):
File "desk/labels.py", line 2, in <module>
from .tickets import Ticket
ImportError: attempted relative import with no known parent package
The line number matches the from .tickets line in the file you saved. The fix is not to rewrite the import as from tickets import Ticket (that breaks python -m desk later). The fix is to run a module as a package: uv run python -m desk.labels will still do nothing useful because labels.py has no main. Put a program next to the package (show_label.py) or add __main__.py (next chapter).
The boring rule
- Application layout:
desk/__init__.py,desk/tickets.py, a program beside the folder orpython -m desk. __init__.pymay be empty. Re-export only a short public list.- Sibling imports:
from .tickets import Ticket. - Do not run
uv run python desk/labels.py. You lose the package. - Do not skip
__init__.pyto look modern. Namespace packages are for split distributions, not for your desk app. __all__is a list of strings, not a substitute for a README.
Try this
- Add
desk/money.pywithdef with_tax(cents, rate=0.1): return round(cents * (1 + rate)). Import it fromshow_desk.pyasfrom desk.money import with_taxand printwith_tax(400). - Re-export
labelfromdesk/__init__.py. Calldesk.label(desk.Ticket(1, 2))from a tiny program. - In
desk/labels.py, tryfrom tickets import Ticket(no dot), runshow_label.py, and read theModuleNotFoundError. Put the dot back. - Add
tools/bell.pywithdef ring(): return "ding"and printtools.bell.ring()fromshow_tools.py.