PYTHON / MODULES AND PACKAGES
Writing your own module
Author a .py file as a real importable module: docstring, public API via __all__, private helpers, and a body that defines rather than does.
What you will learn
- Name module files as lowercase identifiers and never shadow stdlib names like random.py
- Give the file a module docstring; it becomes __doc__ and is what help() prints
- Declare __all__ and prefix internal helpers with _ to mark the public surface
- Keep the module body to definitions so importing stays cheap and free of side effects
Understanding Writing your own module
A module is nothing more than a .py file, and the first time something imports it Python executes that file top to bottom, then keeps every name the file created as an attribute of a module object. That single fact drives every design decision: the top level of your file is running code, not a declaration section. Statements like def, class, and CONSTANT = 5 are cheap because they just bind names, while opening a file or querying a network at the top level happens as a side effect of the word import.
Because the file is a namespace you are designing for someone else, spend the first line on a module docstring: it lands in module.__doc__ and is the header help(module) shows. Inside, promote a small set of functions and constants as the public surface and prefix everything else with a single underscore, which is the universal Python signal for "internal, may change". Adding __all__ = [...] names that public surface explicitly, which controls what from mymodule import * pulls in, though it is documentation rather than enforcement since attribute access still reaches every name.
The filename is the import name, so it must be a valid identifier: money_utils.py imports fine, money-utils.py cannot be imported with the import statement at all, and a file named json.py sitting next to your script will be found before the standard library's json. Since the module object is cached after the first import, any dict, list, or open connection created at module level becomes a process-wide singleton shared by every importer, which is useful for a registry and a trap for anything you expected to be per-caller.
from pathlib import Path
import sys
# Stands in for creating tempconv.py in your editor.
Path("tempconv.py").write_text('''"""Convert between temperature scales."""
__all__ = ["c_to_f", "f_to_c"]
ABSOLUTE_ZERO_C = -273.15
def c_to_f(celsius):
"""Return degrees Celsius converted to Fahrenheit."""
if celsius < ABSOLUTE_ZERO_C:
raise ValueError("below absolute zero")
return celsius * 9 / 5 + 32
def f_to_c(fahrenheit):
"""Return degrees Fahrenheit converted to Celsius."""
return (fahrenheit - 32) * 5 / 9
''')
sys.path.insert(0, ".") # the folder holding the file must be importable
import tempconv
print(tempconv.c_to_f(100))
print(tempconv.__doc__)
print(tempconv.c_to_f.__doc__)
print(tempconv.__all__)
print(tempconv.ABSOLUTE_ZERO_C)A module is a file whose top level executes exactly once on first import, so it should contain definitions and a documented public surface rather than work.
Worked examples
Do work lazily, not at import time
Shows that the module body runs once on first import, so expensive setup belongs inside a function.
from pathlib import Path
import sys
Path("loader.py").write_text('''print("loader body running")
DATA = None
def get_data():
"""Build the table on first use instead of at import time."""
global DATA
if DATA is None:
print("building table")
DATA = {n: n * n for n in range(4)}
return DATA
''')
sys.path.insert(0, ".")
import loader
import loader # cached: the body does not run again
print(loader.get_data())
print(loader.get_data())Example explained
Line 1"loader body running" prints once even though import loader appears twice, because the executed module is cached.
Line 2DATA = None at module level is a cheap placeholder; the real dict is built only when get_data() is first called.
Line 3"building table" appears once, proving the second call reuses the module-level DATA.
Line 4If the comprehension had been written at the top level, every importer would pay for it whether or not they used it.
Module-level state is one shared object
Demonstrates that a dict defined at module level is a single process-wide object every importer mutates.
from pathlib import Path
import sys
Path("registry.py").write_text('''_handlers = {}
def register(name, func):
_handlers[name] = func
def names():
return sorted(_handlers)
''')
sys.path.insert(0, ".")
import registry
from registry import names
registry.register("upper", str.upper)
print(names())
print(registry._handlers is sys.modules["registry"]._handlers)Example explained
Line 1_handlers = {} runs once, so there is exactly one dict no matter how many files import registry.
Line 2names imported by from-import is the same function object, so it sees the mutation made through registry.register.
Line 3sys.modules["registry"] is the cached module object, and the identity check confirms there is no second copy of the state.
Line 4The leading underscore tells readers _handlers is an implementation detail, even though nothing stops access.
__all__ shapes star imports only
Shows that __all__ limits from module import * while normal attribute access still reaches every name.
from pathlib import Path
import sys
Path("geometry.py").write_text('''"""Area helpers."""
__all__ = ["circle_area"]
PI = 3.14159
def circle_area(r):
return PI * r * r
def _validate(r):
return r >= 0
''')
sys.path.insert(0, ".")
ns = {}
exec("from geometry import *", ns)
print(sorted(k for k in ns if not k.startswith("__")))
import geometry
print(geometry.PI)
print(geometry._validate(-1))Example explained
Line 1The star import copies only circle_area, because __all__ lists exactly that one name.
Line 2PI is skipped by the star import even though it is a perfectly ordinary public-looking constant.
Line 3geometry.PI still works, showing __all__ is a curation of the star-import surface, not access control.
Line 4_validate is reachable too; the underscore is a convention that tools and reviewers respect, not a barrier.
Important notes
A leading underscore and __all__ are conventions, not privacy: any importer can still reach _helper via attribute access.
Keep top-level imports in your module minimal and avoid importing a module that imports you back, since both bodies run during import and a half-built module can raise AttributeError.
Common mistakes
Naming the file after a stdlib module (random.py, string.py, csv.py) next to your script: your file wins the import, and unrelated code that expected the real module fails with AttributeError or a strange TypeError.
Leaving demo or test calls at the top level of the module: they execute on every import, printing noise or writing files as a side effect of someone else's import statement.
Editing the module file and re-running import in the same interactive session: the cached module object is reused and your changes appear to be ignored until you restart the interpreter or call importlib.reload.
Try it yourself
Change, predict, then run
Write a module file moneyfmt.py with a module docstring, a constant CURRENCY = "USD", a public function format_amount(cents) that returns strings like "$12.34", a private helper _split(cents), and __all__ = ["format_amount"]. Then import it and print moneyfmt.__doc__, moneyfmt.__all__, and moneyfmt.format_amount(1234).
Open the Python workspaceCheck your understanding
A module report.py opens a database connection as a top-level statement. Your program imports report in three different files but never calls any function from it. What happens?
- The connection is opened once, during the first import, even though no function is ever called
- The connection is opened three times, once per importing file
- The connection is not opened, because module bodies only run when one of their functions is called
- The connection is opened only if report.py is run directly as a script
Show answer
Importing executes the module body top to bottom, so the connection statement runs even with no function calls. It runs only once because the module object is cached and later imports reuse it, which rules out the "three times" answer; module bodies are not lazy, which rules out the third.