Split a working script into modules, trace import search and caching, diagnose import failures, and keep reusable behavior separate from program entry points.
You will learn: Create and import real modules, trace namespaces and the import cache, inspect search locations and failures, and design a main boundary that supports both reuse and execution.
Practice in: A local editor and terminal; temporary workspaces also run in Colab or JupyterLab
Your constellation report has grown into one long script. Coordinate conversion, label formatting, report assembly, demo data, and printing are mixed together. Changing one part means scrolling through all the others, and importing one useful function would run the whole report. This lesson separates those responsibilities without turning every function into its own microscopic file.
Keep five questions visible:
What namespace does each import form create in the caller?
Which statements run the first time a module is imported?
Why does editing a module not automatically change an already imported object?
Which search location supplied the module?
How can one file offer reusable functions and still run as a program?
1. A module is an executed file with its own namespace
Create a folder named constellation-report containing this file:
# orbit_math.pyDEGREES_IN_CIRCLE =360def normalize_degrees(value):"""Return an angle in the half-open range 0 <= result < 360."""return value % DEGREES_IN_CIRCLE
A .py file is a module source file. When Python imports it, Python creates a module object, gives that object a namespace, executes the file’s top-level statements in that namespace, and caches the module object.
Add a second file beside it:
# report.pyimport orbit_mathangles = [-15, 20, 390]normalized = [orbit_math.normalize_degrees(angle) for angle in angles]print(normalized)
From inside constellation-report, run:
python report.py
The expected output is:
[345, 20, 30]
Trace the important names:
Namespace
Name
Value or role
orbit_math module
DEGREES_IN_CIRCLE
360
orbit_math module
normalize_degrees
function object
report module
orbit_math
module object
report module
angles
input list
report module
normalized
output list
import orbit_math does not copy every module name into report. It binds one name, orbit_math, and attribute access crosses the visible module boundary. That explicit qualifier helps a reader locate ownership.
An import binds a module object in the caller while the imported definitions remain in the module’s own namespace.
# noisy_orbits.pyprint("Loading demo observations...")def normalize_degrees(value):return value %360
Any importer displays the message even if it only wants the function. Imports must execute definitions, assignments, and import statements needed to create the module. Avoid unrelated printing, input prompts, file writes, network calls, and expensive demo work at import time.
The direct binding scale_copy still refers to the earlier integer. A from module import name statement similarly binds the object available at import time; it is not a permanently live link to later rebinding in the module.
For ordinary constants and functions this is usually fine. During debugging, however, it explains why changing or rebinding a module attribute does not update every name copied elsewhere.
3. The import cache prevents repeated initialization
Python records initialized modules in sys.modules. A later import of the same name normally returns that cached object rather than executing the source file again.
Use a child process and temporary file so the demonstration starts clean:
The statement appears once, not twice. The cache supports shared module state and avoids repeated setup. It also creates a notebook trap: after editing a source file, running import beacon again in the same kernel may reuse the old module. Restarting the kernel or process gives the cleanest beginner proof. importlib.reload exists, but existing from ... import ... bindings and created objects make reload behavior more subtle than “restart the file.”
Inspect the cache without mutating it:
import jsonimport sysprint("json"in sys.modules)print(sys.modules["json"] is json)
Deleting arbitrary cache entries is not a normal repair strategy. Find the source of stale state and reproduce in a fresh process.
An import name is not a filesystem path. Python asks configured import finders to locate it. For ordinary source imports, sys.path displays the search path:
The exact entries depend on how Python started, the environment, the platform, and configuration. The script directory or current working directory commonly appears near the beginning, followed by standard-library and environment locations. This is why project files can shadow installed or standard modules.
Use the import system to report a candidate origin:
from importlib.util import find_specfor name in ["pathlib", "orbit_math"]: specification = find_spec(name)print(name, specification.origin if specification else"not found")
In a notebook without orbit_math.py on its search path, the second result is not found. That is expected—not evidence that Python itself is broken.
Classify common import failures
These failures point to different places:
Symptom
Likely question
ModuleNotFoundError: No module named 'orbits'
Is the top-level name installed or on the search path for this interpreter?
ImportError: cannot import name 'scale' from 'orbits'
Was the module found but the selected attribute absent or not yet initialized?
Attribute missing from an unexpected local path
Is a file or package shadowing the intended module?
Import works only from the repository root
Is the code relying on the working directory instead of installation?
Import behaves differently after edits
Is an old module cached in this process?
Do not automatically append parent directories to sys.path inside project source. That hides a broken project or command contract and makes behavior depend on a guessed folder depth. Later in this unit, a src layout plus an editable installation will make the package available intentionally.
Separate discovery failure from module-execution failure
ModuleNotFoundError does not always mean the first import name was absent. Read the traceback and the exception’s name:
Now imagine constellation was found but its source contains import missing_catalog. The traceback begins while executing constellation, but the missing name is missing_catalog. Installing another copy of constellation does not address that dependency.
An import can also find a file and then fail during its top-level execution:
# broken_catalog.pyDEFAULT_SCALE =0def adjusted(value):return value / DEFAULT_SCALE# Avoid top-level demonstration work like this:# preview = adjusted(10)
If the commented preview ran, discovery would have succeeded and initialization would have raised ZeroDivisionError. Preserve the full traceback: the exception category and failing source line distinguish “not found” from “found but broken while importing.”
Bytecode caches are outputs, not source ownership
Python may create __pycache__/ and .pyc files after imports. They store bytecode tagged for an interpreter implementation/version so later starts can avoid recompiling unchanged source. They do not replace the .py source as the project’s maintained contract.
The exact tag varies, which is one reason caches do not belong in Git. Python normally checks cache validity against source metadata. When a rename or unusual tool leaves the diagnosis confusing, remove generated caches and reproduce in a fresh process—but do not treat routine cache deletion as the repair for an incorrect import graph.
Reproduce two search locations without changing the parent notebook
This lab proves that the same import spelling can select different files:
Each child begins with a clean cache and a different starting location. Record both origins. In a real project, installation and distinctive package names remove this ambiguity; adding another guessed search directory would increase it.
Capture a useful import report
import importlib.utilimport sysfrom pathlib import Pathdef import_report(name):"""Return interpreter, working directory, and discovered origin for a name.""" specification = importlib.util.find_spec(name)return {"name": name,"interpreter": sys.executable,"working_directory": str(Path.cwd()),"origin": specification.origin if specification elseNone, }print(import_report("json"))
This diagnostic observes configuration without changing it. Preserve the exact command alongside the report because script mode and -m mode establish different entry context.
5. __name__ separates import from direct execution
Every module receives a __name__. When imported normally, it is the import name. When Python executes a source file as the top-level program, it is "__main__".
# constellation_report.pydef build_report(labels):"""Return one display line for constellation labels.""" cleaned = [label.strip().title() for label in labels]return"Constellations: "+", ".join(cleaned)def main():"""Run the small demonstration program."""print(build_report([" lyra", "orion "]))if__name__=="__main__": main()
The if line does not make main special. It simply compares a module-provided name with a string. During import, Python defines the functions but skips the call. During direct execution, the condition is true and the call runs.
The first prints the demo once. The second prints only the expression explicitly requested by the importer.
Keep the guarded block tiny. Put behavior in a callable so other Python code can reuse it and later tests can call it without launching a process. Unit 12 will develop full argument parsing, streams, and exit status; here, main only marks the execution boundary.
6. python -m executes a discovered module
The command:
python-m constellation_report
asks the selected interpreter to find constellation_report through the import system and execute it as __main__. This differs from passing a filesystem path such as python tools/constellation_report.py. Module mode works with the importable project structure and later supports packages through a package/__main__.py file.
Use python -m pip for the same identity reason: it asks the displayed Python interpreter to run its importable pip module. A bare pip command may be a launcher associated with another installation.
Compare the two launch contracts explicitly
From a directory containing constellation_report.py, run these diagnostics:
Both execute the module as __main__, but the first names a source path and the second asks the import system to locate a module name. Depending on the command and platform, the first search entry can reflect the script directory or the current directory. Do not build application behavior around the incidental text of that entry. Use the commands to understand why one undocumented launch location can mask an import problem.
Write a launch contract in the README when a project gains a supported command:
Working directory: project rootInterpreter: project environment PythonCommand: python -m constellation_reportInput: built-in demonstration observationsOutput: report text on standard output
This record makes reruns comparable. “I clicked Run in my editor” omits the interpreter, working directory, and launch mode, all of which can affect module discovery.
After the comparison, remove diagnostic prints from reusable source. Keep a small main and use an external command or logging/debugging tool when future investigation needs the same evidence. Importers should not inherit permanent diagnostic noise.
Editor Run buttons encode these same choices through settings: selected interpreter, launch file or module, working directory, arguments, and environment variables. When terminal and editor behavior differ, display that configuration rather than claiming editors use a different kind of Python. Reproduce the editor command in a terminal with the same interpreter and working directory. If it then matches, the source is probably not the differing variable. If it does not, compare the exact environment and arguments next.
Keep the launch record beside the project while diagnosing, then turn the supported route into concise README instructions. Debug-only variations such as running an internal module path should not become accidental public commands. One documented route, one fresh-process verification, and one callable action owner give future learners a stable place to begin.
WarningDo not combine -m with a .py path
python -m constellation_report takes a dotted import name without .py or path separators. python constellation_report.py takes a filesystem path. They are two different command forms.
7. Build the multi-file constellation report
Create this structure in a disposable local folder:
Angle normalization remains in orbit_math; display policy remains in report_text; orchestration remains in run_report. The files are separated by responsibility rather than by arbitrary line count.
Checkpoint: choose a dependable execution boundary