Modules and Imports
Context
Any Python file can be loaded by another Python file, which makes the first one's functions available to the second. The loaded file keeps its own namespace, so its names stay grouped under it rather than scattering into whoever imported it.
This is how a script stops being one file. Loading, cleaning, metrics, and reporting each move into their own file, and the script that runs the pipeline pulls in what it needs from each. The same cleaning file then serves every script in the project without being copied, which is the point at which reusable work actually becomes reusable.
The failures are about where Python looks. A module that imports fine beside one script is invisible to a script that lives elsewhere, because the search starts in the launched script's own folder. The other trap is shadowing, a file named after a standard-library module such as json or random gets found first, and the real one becomes unreachable with an error that blames a missing name rather than the file. The previous section made code survive bad input, this lesson makes it survive being used from somewhere else.
Goal
- Split a single script into multiple
.pyfiles and import names between them. - Apply the three import forms (
import module,import module as alias,from module import name) and distinguish when each is appropriate. - Trace how Python locates the file behind an
importusing the search path. - Explain why each module keeps its own separate namespace and predict whether a name leaks between files.
- Identify and break a circular import, then restructure the files to fix it.
Prerequisites
Optional, neither is a gate. The lesson stands on its own, they are here if you want the shape of an import in front of you before you write one.
- YouTube - "Python Tutorial for Beginners 9: Import Modules and Exploring The Standard Library" by Corey Schafer. The first half, on
importagainstfrom-import, for the visual mental model. - Official docs - The Python Tutorial, "More on Modules" and "The Module Search Path". Only those two headings, for how Python finds and runs a module.
Table of Contents
- Module (.py file)
- import
- from-import
- Module Namespace
- Import Resolution
Theory
Module (.py file)
What. A module you write is a single .py file used as a reusable, importable unit of Python code.
Why. Once a program outgrows one file, you split related code (data loading, transforms, metrics) into separate modules so each file stays small and navigable.
When. The moment a script grows past a few hundred lines, or the moment two scripts need to share the same function, you move that shared code into a module.
Every .py file you have ever written is already a module. The module's name is simply the filename with .py removed. A file called metrics.py is the module metrics. There is no special keyword that turns a file into a module, being a .py file on disk is enough. When another file imports it, Python runs that file top to bottom one time and keeps the resulting names around.
A module holds two kinds of things you care about: the functions you def inside it, and the module-level variables you assign at the top (often constants). Both become reachable from other files once imported.
# === file: metrics.py ===
# this whole file is a MODULE named 'metrics' (filename minus '.py')
# 'mean' returns the average of a list of numbers
def mean(values):
# 'sum(values)' adds every element, 'len(values)' counts them
return sum(values) / len(values)
# a module-level constant, part of this module's names
DEFAULT_PRECISION = 4Notice there is no print and no call at the top level. A well-behaved module file defines things rather than doing work the instant somebody loads it.
DON'T:
- DON'T name a module file the same as a standard-library module, for example
random.pyorjson.py, because for modules like these your file hides the real one. - DON'T put spaces or hyphens in a module filename like
data-loader.py, becauseimport data-loaderis not valid Python.
import
What. import module runs the target file once and binds a single name, the module object, that you reach through with a dot.
Why. It keeps the origin of every name visible, metrics.mean tells you instantly that mean came from metrics, which matters as a project grows.
When. Use plain import as the default form whenever you want clarity about where names come from, especially across many modules.
After import metrics, the name metrics refers to the whole module object. To reach anything inside it you go through that object with dot access, metrics.mean or metrics.DEFAULT_PRECISION. Once an import of metrics succeeds, writing import metrics again never re-runs the file.
# === file: main.py ===
# 'import metrics' finds 'metrics.py', runs it once, binds the name 'metrics'
import metrics
dataset = [0.91, 0.88, 0.94] # model accuracy scores, one per run
# 'metrics.mean' reaches the 'mean' function THROUGH the module namespace
print(metrics.mean(dataset)) # 0.91
# 'metrics.DEFAULT_PRECISION' reaches the module-level constant
print(metrics.DEFAULT_PRECISION) # 4The dot is doing namespace lookup, "find the name mean inside the metrics module". Nothing from metrics is available without that prefix, which is exactly the point of this form.
DON'T:
- DON'T assume
import metricsre-reads the file on every call, it runs the file only on the first import and reuses the result afterward. - DON'T write
import metricsinside a tight loop expecting a fresh load each time, the loop will just rebind the same cached module.
from-import
What. from module import name runs the module on its first import, as plain import does, but binds only the specific name(s) you ask for, with no dot prefix.
Why. It removes repetitive prefixes when you use one or two names from a module a lot, mean(x) reads cleaner than metrics.mean(x) in a function full of calls.
When. Use it when you need a small, fixed set of names from a module and the source is obvious from context.
The trade-off is that you lose the module label at the call site. mean(dataset) no longer says where mean came from, and if two modules both define mean, a second from-import silently overwrites the first. That is the price of the shorter name.
# 'from metrics import mean' pulls ONLY 'mean' into this file's names
from metrics import mean
predictions = [1.0, 0.5, 0.5] # three predicted probabilities
# 'mean' is now a local name here, no 'metrics.' prefix needed
print(mean(predictions)) # 0.6666666666666666
# pull several names at once, separated by commas
from metrics import mean, DEFAULT_PRECISIONThe module metrics still runs in full either way. All the from-import changes is which of its names land in your file.
DON'T:
- DON'T write
from module import *, it dumps every public name into your file and makes it impossible to see at a glance where a name came from. - DON'T from-import a name that collides with a built-in, for example
from os import open, because it shadows Python's ownopenand every lateropen("file.txt")in that file calls the low-levelosversion instead, which takes different arguments and raises aTypeError.
Module Namespace
What. A module namespace is the separate container of names that each module's top-level assignments and defs live in.
Why. It is why two files can both define THRESHOLD without clashing, each name belongs to its own module and never leaks into another.
When. You reason about namespaces every time you wonder "is this name visible over here?", the answer is no unless you imported it.
What earlier lessons called a module's global scope is more precisely this module namespace. Each module gets its own. A name defined at the top of train.py is not magically visible inside evaluate.py. The only way one module sees another's names is by importing them, and even then they stay behind the module's dot prefix unless you from-import.
# === file: train.py ===
THRESHOLD = 0.5 # this 'THRESHOLD' lives in the 'train' namespace
# === file: evaluate.py ===
THRESHOLD = 0.9 # a DIFFERENT 'THRESHOLD' in the 'evaluate' namespace
# === file: main.py ===
# import both modules, each keeps its own separate names
import train, evaluate
# the two 'THRESHOLD' names never collide, each reached via its module
print(train.THRESHOLD) # 0.5
print(evaluate.THRESHOLD) # 0.9The dot is what selects which namespace you mean, and it is the only thing keeping two identically spelled names apart.
# 'dir(module)' lists the names that live in a module's namespace
import metrics
# this prints 'mean', 'DEFAULT_PRECISION', plus double-underscore names
print(dir(metrics))Alongside your own names you will see several wrapped in double underscores. Python attaches those to every module it loads from a .py file, so even a two-line file reports a long list.
DON'T:
- DON'T assume a variable defined in one module is visible in another, it is not, importing is the only bridge.
- DON'T expect
importto merge two modules' names into one shared pool, each keeps its own namespace behind its own prefix.
Import Resolution
What. Import resolution is the search procedure Python runs to find the actual file behind an import name.
Why. Knowing the search order explains real failures, why import metrics says "module not found", or why your random.py hijacked the standard library.
When. You reach for this knowledge whenever an import behaves unexpectedly, finds the wrong file, or loops back on itself.
When you write import metrics, Python first checks a cache of already-loaded modules, then the modules built into the interpreter itself, such as sys. If the name is in neither, Python walks an ordered list of directories and uses the first match it finds. That list always starts with the directory of the script you launched, then continues with any directories from the PYTHONPATH environment variable, then the standard-library and installed-package locations. This is the same idea as the executable lookup through PATH you met in Module 0, a list of places, checked in order, first hit wins.
# 'sys' (standard-library module) exposes the import machinery
import sys
# 'sys.path' is the ORDERED list of folders Python searches for a module
for entry in sys.path:
# the first entry is usually the launched script's own folder
print(entry)The first line of output is normally the folder of the script you ran, which is exactly why a file named random.py sitting next to your script beats the real random module. Your folder is searched first, so your file matches before the standard library is ever reached.
# 'sys.modules' is the CACHE of already-imported modules, a dict
import sys
import metrics # first import: cached in sys.modules, then the file runs
import metrics # second import: NO re-run, name rebound from the cache
# check whether 'metrics' is sitting in the cache
print("metrics" in sys.modules) # TrueThe two import metrics lines do very different amounts of work, and the cache is why. It is also why a module's top-level code runs on its first import and never on a later one, and why editing a module mid-run changes nothing until you restart. The script you launch is run rather than imported, so if a module it imports imports it back, its file runs a second time.
A circular import is the trap that catches people hardest here. If a.py does from b import ... at the top, and b.py does from a import ... at the top, then importing either one forces Python to pull a name out of a module that is only half-built. That name has not been defined yet, so you get an ImportError reading cannot import name ... from partially initialized module ... (most likely due to a circular import).
# === file: a.py === (DO NOT run, shown to illustrate the trap)
# from b import split # needs the NAME 'split' before 'b' has finished
# def load(): ...
# === file: b.py ===
# from a import load # 'a' is only half-built, so 'load' does not exist yet
# def split(): ...Each file tries to fully load the other before finishing itself, which is impossible. The fix is to break the loop: move the import inside the function that needs it, merge the two files, or extract the shared piece into a third module that both import without importing each other.
DON'T:
- DON'T name your file after a standard-library module, for one such as
jsonorrandomyour folder is searched first so your file silently replaces the real one. - DON'T let two modules
importeach other at the top level, restructure so the dependency goes one direction only.
Lab Exercises
Lab Exercise 1: Two Files, Two Import Styles, Then a Second Module
Create geometry.py defining a module-level constant PI = 3.14159 and a function area_circle(radius). From a second file main.py, compute one area using the plain import form and another using the from-import form, and print both.
Then add stats.py holding mean(values) and spread(values), where spread is the maximum minus the minimum. Import it into the same main.py and print a one-line summary of dataset = [12, 7, 20, 5] using both functions through the module prefix. Add a comment saying which of the two call styles keeps the origin of a name visible at the call site.
Lab Exercise 2: Namespaces, What a Module Exposes, and the One-Time Run
Create train.py and evaluate.py, each defining BATCH_SIZE with a different value, and give train.py one top-level print announcing that it ran.
From main.py, import both and print each BATCH_SIZE to show they do not collide. Then print the names train exposes, filtered so the double-underscore ones are left out, and say in a comment who put the filtered-out ones there.
Then print the first entry of sys.path and state in a comment what folder it is. Finally, write import train a second time, print whether "train" is in sys.modules, and add a comment explaining why train.py's announcement appeared only once even though the file was imported twice.
Solution 1: Two Files, Two Import Styles, Then a Second Module
# === file: geometry.py ===
PI = 3.14159 # a module-level constant, part of the 'geometry' namespace
# 'area_circle' returns the area for a given radius
def area_circle(radius):
return PI * radius * radius# === file: stats.py ===
def mean(values):
return sum(values) / len(values) # total divided by how many there are
def spread(values):
return max(values) - min(values) # how far the data ranges end to endNeither file does anything on its own. Both only define names, which is what a module is for, and the caller decides when any of it runs.
# === file: main.py ===
import geometry
from geometry import area_circle # binds the FUNCTION name, not the module
import stats
print(geometry.area_circle(2)) # 12.56636
print(area_circle(4)) # 50.26544
dataset = [12, 7, 20, 5]
print("mean:", stats.mean(dataset), "spread:", stats.spread(dataset))
# mean: 11.0 spread: 15
# 'geometry.area_circle' and 'stats.mean' carry their source in the call
# itself. The bare 'area_circle' does not, so a reader has to go back up to
# the import line to learn where it came from.DON'T:
- DON'T expect the second import of a module to re-run its file, the first one already cached it.
- DON'T move
datasetintostats.py, a shared module holds reusable logic and not one caller's data.
Solution 2: Namespaces, What a Module Exposes, and the One-Time Run
# === file: train.py ===
print("train.py is running") # top-level code, runs on the FIRST import
BATCH_SIZE = 32
# === file: evaluate.py ===
BATCH_SIZE = 64 # a DIFFERENT 'BATCH_SIZE', in the 'evaluate' namespace# === file: main.py ===
import sys
import train
import evaluate
print(train.BATCH_SIZE) # 32
print(evaluate.BATCH_SIZE) # 64
# 'dir' lists a namespace, the filter drops the names Python attaches itself.
# ['BATCH_SIZE']
print([n for n in dir(train) if not n.startswith("_")])
print(sys.path[0]) # the folder of the launched script
import train # the SECOND import of the same module
print("train" in sys.modules) # True
# train.py is running printed once, above the 32, and never again. The
# first import stored the module in 'sys.modules' and ran the file, and the
# second found it sitting there and only rebound the name.The filtered-out names are Python's own. Every module loaded from a .py file gets __name__, __file__ and several more attached to it automatically, so dir on even this two-line module returns a long list of which one entry is yours.
DON'T:
- DON'T read
BATCH_SIZEwithout a prefix here, no bareBATCH_SIZEexists inmain.py. - DON'T take
sys.path[0]for the current working directory, it is the launched script's own folder and the two differ the moment you run a script from somewhere else.
Theory Assessment
Q1: What Makes a File a Module, and the Two Ways to Reach Inside One
Explain what a .py file needs in order to be importable, and what its module name will be. Then state the difference between import metrics and from metrics import mean, in terms of which names become available in the importing file and how each is called, and say which of the two keeps the origin of a name visible.
Hint: There is no keyword and no registration step, only a filename and a location. For the second half, one form binds the module object and the other binds a name from inside it.
Q2: Why Two Modules Can Use the Same Name
Two modules each define LEARNING_RATE. Explain why importing both into a third file causes no conflict, and how you reach each one. Then say what dir(module) reports, and why most of what it lists is not something you wrote.
Hint: Each module owns a private container of names. For the second half, ask who put the double-underscore entries there.
Q3: Where Python Looks, and How Often It Looks
You have a file named json.py in your project folder and you write import json. State what actually gets imported and why, naming the rule that decides it. Then say how many times metrics.py executes in a program that imports metrics from three different files, and which mechanism is responsible.
Hint: The search is an ordered walk through directories, and one directory is always first. For the second half, the mechanism is a dictionary.
Q4: One Line Changed in Another Module
Read the following without running it, then state every line of output in order.
# === file: config.py ===
print("config loaded")
LIMIT = 10
def show():
print("LIMIT is", LIMIT)
# === file: main.py ===
import config
from config import LIMIT, show
config.LIMIT = 99
print(LIMIT)
config.show()
show()Two statements import the same file and one line rebinds a name inside it. Say how many times config.py runs, what print(LIMIT) shows and why it disagrees with what config.LIMIT now holds, and why both calls to show agree with each other instead.
Hint: from config import LIMIT points a name in main at whatever config.LIMIT pointed at when the import ran, and nothing moves it afterwards. A function looks its globals up in its own module every time it is called.
Answer 1:
A .py file whose name is a valid Python name, with no spaces or hyphens, is importable simply by existing on Python's search path, with no keyword, marker or registration, unless Python finds another module of that name first, as it finds sys and os before it searches any folder. Its module name is the filename with .py removed, so metrics.py imports as metrics, and the first successful import runs the file top to bottom exactly once.
import metrics binds one name, the module object, and everything inside is reached through a dot as metrics.mean(...). from metrics import mean runs the same file but binds only the bare name mean, called directly with no prefix. The plain import keeps the origin visible at every call site, which is what makes it the safer default, and the from-import trades that visibility for a shorter name and for the risk that a later from-import of the same name silently replaces the first.
DON'T believe a from-import skips running the module, on a first import it executes the whole file and only chooses which names it hands you.
Answer 2:
Each module owns a separate namespace, a private container for its top-level names, so two modules can both define LEARNING_RATE without sharing storage. From the importing file you disambiguate with the module prefix, train.LEARNING_RATE against tune.LEARNING_RATE, and there is nothing left to collide.
dir(module) reports the names living in that module's namespace. Most of what it lists is not yours, it is the double-underscore names Python attaches automatically to every module loaded from a .py file, __name__ and __file__ among them. Filtering those out is what leaves the names the file actually defined.
DON'T expect importing two modules to merge their names into one pool, the prefixes are the whole reason they stay apart.
Answer 3:
Your own json.py is imported, not the standard library's. Python walks an ordered list of directories and the launched script's own folder comes first, so the local file matches before the standard-library location is ever reached. The rule is first match wins, the search stops at the first directory holding a match.
metrics.py executes exactly once. The first import stores the module in the sys.modules cache dictionary and runs the file, and the second and third find it there and rebind the name without re-running anything. That cache is why a later import never runs a module's top-level code again, and why editing a module mid-run changes nothing until you restart.
DON'T name a local file after a standard-library module, for one such as json the search order guarantees yours wins, and the error that follows blames a missing name rather than your file.
Answer 4:
The output is four lines:
config loaded
10
LIMIT is 99
LIMIT is 99config.py runs once. Two statements import it, but the first caches it and runs the file, and the from config import ... finds it already in sys.modules, so config loaded prints a single time.
print(LIMIT) shows 10 because from config import LIMIT took what the name pointed at at the moment of import, binding main's own LIMIT to the integer 10. Rebinding config.LIMIT = 99 moves a tag inside config's namespace and cannot reach across into main's, so the two names have been independent since the import line ran.
Both calls to show print 99 because a function looks its globals up in its own module every time it runs, never in whoever called it. show lives in config, so it reads config.LIMIT, which is now 99. And show and config.show are the same function object, since the from-import copied a reference to it rather than a copy of it, so the two could never have disagreed.
The trap is that one statement bound both names the same way, as tags on what config held at import time, and only one of them goes back to config afterwards. LIMIT stays on 10 because nothing re-points it, while show looks LIMIT up in config every time it runs.
DON'T from-import a name another module will rebind later, you get a snapshot rather than a live view, import the module and read config.LIMIT through it when you need the current value.
Assignment
Assignment: Break and Fix a Circular Import
Deliberately create a circular import between loader.py and cleaner.py, where each uses a from-import to pull a name from the other at the top level. Run it and capture the real error text rather than describing it, then fix the cycle by extracting the shared helper into a third module common.py that both import. Additionally, in the fixed version, import common under an alias, written import common as c, which binds the module to the shorter name c, and call its helper through that alias.
Hints:
- The error names a name that is not available yet during the half-finished import, read it carefully before you fix anything.
- The cure is a one-directional dependency:
loaderandcleanerboth depend oncommon, andcommondepends on neither.
Solution:
# === file: loader.py === (BROKEN circular version, run it to see the error)
from cleaner import clean # needs the NAME 'clean' before 'cleaner' has finished
def load():
return clean([1, -2, 3])
# === file: cleaner.py === (BROKEN circular version)
from loader import load # 'loader' is half-built, 'load' does not exist yet
def clean(values):
return [abs(v) for v in values]
# === file: run_broken.py ===
import loaderRunning run_broken.py produces this, and reading it before fixing anything is the whole BREAK step:
Traceback (most recent call last):
File "/home/goku/dev/bootcamp/run_broken.py", line 1, in <module>
import loader
File "/home/goku/dev/bootcamp/loader.py", line 1, in <module>
from cleaner import clean # needs the NAME 'clean' before 'cleaner' has finished
^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/goku/dev/bootcamp/cleaner.py", line 1, in <module>
from loader import load # 'loader' is half-built, 'load' does not exist yet
^^^^^^^^^^^^^^^^^^^^^^^
ImportError: cannot import name 'load' from partially initialized module 'loader' (most likely due to a circular import) (/home/goku/dev/bootcamp/loader.py)The message is precise once you know what to look for. loader is described as partially initialized, which is the state a module is in while its own top line is still running, and load is genuinely not defined yet because the def two lines below has not been reached.
# === file: common.py ===
# the shared helper now lives here, and imports NOTHING from either file
def clean(values):
return [abs(v) for v in values] # absolute value of each element# === file: loader.py === (FIXED version)
import common as c # the module bound under a short alias
def load():
return c.clean([1, -2, 3]) # [1, 2, 3]
# === file: cleaner.py === (FIXED version)
import common as c
def clean_all(rows):
return [c.clean(r) for r in rows] # the same helper, reached the same wayBoth files now depend on common and common depends on neither, so the graph has no loop left to walk. The alias c is a local rename of the module object and nothing more, and it still points a reader back at common, which is the only test an alias has to pass.
The deeper point is that a cycle is a design symptom rather than a syntax problem. Two modules importing each other means neither owns the shared piece, and the error is Python noticing that before you do. Moving the import inside a function would also silence it, and it silences the symptom while leaving the tangle in place, which is why extracting the shared code is the fix worth writing.
DON'T fix a cycle by burying imports inside functions and hoping, decide the dependency direction first, then extract the shared code so the graph genuinely has no loop.
Additional Resources
Optional, and nothing later depends on either. Worth opening if you want the machinery behind the cache rather than its effects.
- Official docs - Python Language Reference, "The module cache". What
sys.modulesactually is and how an import consults it. - Official docs -
importlib, thereloadentry. The precise behavior of forcing a cached module to re-run, for when you outgrow restarting.
Summary
A program stops being a single script the day its parts start needing each other in different files, and that is the moment modules earn their place. What you have done here is small in mechanics, a few import lines, but large in consequence. You have learned that a .py file is already a unit you can pull from, that names do not float freely between files but sit inside private namespaces, and that the dot you type is the deliberate act of reaching across one of those boundaries. The structure of a real project is nothing more than many such reaches, arranged so the code flows in a direction you can follow.
The quieter lesson is about machinery you cannot see. Python finds your module by walking an ordered list of folders and taking the first match, it runs a module's file on its first import and hands every later import the result it remembered, and it will happily tie two files into a knot if you let them import each other. None of this is visible in the source, which is precisely why it causes confusion the first time it bites. Carrying a correct mental model of the search path and the import cache turns those baffling errors into ordinary, fixable ones.
As your data-loading code, your transforms, and your metrics drift into their own files over the coming lessons, the import line at the top of each module becomes the map of how your project hangs together. Keep that map honest and one-directional, and a growing codebase stays navigable. Let it tangle, and even a small project becomes hard to reason about. The skill you are really building is not typing import, it is deciding what belongs where.
Cheat Sheet
| Concept | Syntax | When to Use | When to Avoid |
|---|---|---|---|
| Module | a .py file |
any reusable chunk of code | never, it is just a file |
| Plain import | import metrics |
you want the source visible via prefix | when the prefix becomes noisy for one heavily used name |
| Aliased import | import metrics as m |
a long or heavily repeated module name | when the alias hides the module's identity |
| from-import | from metrics import mean |
a small fixed set of names | many names, or risk of shadowing built-ins |
| Wildcard | from metrics import * |
almost never | always in real code, it hides origins |
| Namespace check | dir(module) |
inspecting what a module exposes | as program logic, it is for exploring |
| Search path | sys.path |
debugging "module not found" | editing it casually at runtime |
| Module cache | sys.modules |
understanding why a second import runs nothing | manual cache surgery |
# the three import forms, side by side
import metrics # bind module, use 'metrics.mean'
import metrics as m # bind module under alias 'm'
from metrics import mean # bind bare name 'mean'
# inspect resolution machinery
import sys
print(sys.path[0]) # first directory searched (script's folder)
print("metrics" in sys.modules) # True after first import (cached)
print(dir(metrics)) # names living in the module namespace