Import System
The import statement is the visible tip of a whole machinery — search paths, caching, and execution order — that decides which file gets loaded and how it's exposed to your code.
import looks like a single keyword, but underneath it Python runs a defined algorithm: find the module, load and execute it (or reuse a cached copy), and bind names into the current namespace. Understanding that algorithm explains behavior that otherwise looks mysterious — why imports are fast the second time, why circular imports break in specific ways, and why two files with the same name in different folders don't collide.
import module vs from module import name
import module binds one name — the module itself — into your namespace; you reach its contents through that name. from module import name binds specific names directly into your namespace, copying references to them out of the module.
Both forms load the same module once; they differ only in what names end up in your file's namespace.
Star imports are usually discouraged: they make it unclear where a name came from and can silently shadow existing names.
import math
print(math.sqrt(16))
from math import sqrt, pi
print(sqrt(16))
print(pi)from math import * # imports every public name from math directlyAliases with as
import module as alias and from module import name as alias rebind the imported thing under a different local name — useful for shortening long names, avoiding collisions, or following ecosystem conventions.
numpy is a third-party library, not part of the standard library — mentioned here only for the as syntax convention.
import numpy as np # ecosystem convention (third-party)
from collections import OrderedDict as ODThe Import Algorithm, Step by Step
When Python executes import name, it: (1) checks sys.modules for an already-loaded module of that name — if found, binds it and stops; (2) otherwise searches the module search path for something matching that name; (3) if found, creates a new module object, executes the file's code into that module's namespace, records it in sys.modules, then binds the name in the importing scope.
import name
|
v
name in sys.modules? --yes--> bind cached module, done
|no
v
search sys.path for 'name'
|
v
found? --no--> ModuleNotFoundError
|yes
v
create module object, register in sys.modules
execute the module's code into its namespace
bind the name locallysys.modules: the Import Cache
sys.modules is a dictionary mapping module names to already-loaded module objects. It is the reason a module's top-level code runs only once no matter how many files import it: every import after the first is a cache lookup, not a re-execution.
You can manipulate sys.modules directly (e.g. deleting an entry forces a re-import), but this is an advanced technique mostly used in testing frameworks and import hooks, not everyday code.
import sys
import json
print('json' in sys.modules) # True
print(sys.modules['json'] is json) # True: same objectThe Module Search Path (sys.path)
sys.path is the ordered list of locations Python checks when resolving an import: normally the directory of the running script (or the current directory for interactive use), the PYTHONPATH environment variable's entries, and the standard installation directories (including site-packages, where installed third-party packages live). Python searches in order and uses the first match.
This is a Python language behavior (the search-path concept), though the exact default entries are determined by the interpreter and installation, not the language specification itself.
import sys
for p in sys.path:
print(p)Import Execution Runs Once, at First Import
Because of the sys.modules cache, only the first import of a module in a process actually executes its code. This matters for modules with observable side effects and for understanding why changing a module's global state from one place is visible everywhere else that imported it — they all share the same module object.
# config.py
settings = {'debug': False}
# a.py
import config
config.settings['debug'] = True
# b.py (imported after a.py in the same run)
import config
print(config.settings['debug']) # True — same module object, same dictCircular Imports: The Basics
A circular import happens when module A imports module B while module B (directly or indirectly) imports module A. Because each module executes top to bottom and gets registered in sys.modules before it finishes running, a circular import can succeed or fail depending on exactly what point in each file the mutual import happens and what names are needed at that point.
The partially-initialized module is still added to sys.modules before its body finishes, which is exactly why the second import in the cycle sees an incomplete namespace rather than looping forever.
The reliable fixes — restructuring modules, moving an import inside a function, or importing the module (not specific names) and deferring attribute access — are typically covered as part of package design; this topic only introduces the mechanism.
# a.py
import b
def greet():
return 'hello from a'
# b.py
import a
def use_a():
return a.greet() # works fine if called AFTER both modules finish loading
# running a.py first: a starts, imports b; b starts, imports a
# (a is already in sys.modules, but only partially initialized — greet
# is not yet defined) -> b.py's use_a() would fail with AttributeError
# if called immediately, but works if called later, after a.py finishes.Absolute Imports Are the Default
A plain import module or from module import name is an absolute import: it is resolved against sys.path from the top, the same way regardless of which file does the importing. Relative imports (from . import sibling), used inside packages, are a separate mechanism covered in the Packages topic.
Common Import Errors
ModuleNotFoundError means Python could not find anything by that name anywhere on sys.path. ImportError (often cannot import name X from Y) means the module was found, but the specific name requested doesn't exist in it — frequently a symptom of a circular import catching the target module mid-initialization, or simply a typo.
ModuleNotFoundError: No module named 'reqeusts' # typo, or not installed
ImportError: cannot import name 'foo' from 'bar' (bar.py) # foo not defined yet/at allMental Model
Think of sys.modules as a warehouse of already-built module objects and sys.path as the delivery route used to find a module the first time it's ordered. import always checks the warehouse first; only a cache miss triggers a trip down the route, a build, and a delivery.
Key Takeaways
import module binds the module object; from module import name binds specific names directly.
sys.modules caches every loaded module by name, so a module's top-level code executes only once per process.
sys.path is the ordered list of locations Python searches to resolve an import name to a file.
Circular imports can partially work or fail depending on exactly when each module needs a name from the other.
ModuleNotFoundError means the module wasn't found at all; ImportError means it was found but the requested name wasn't in it.
from module import * from module import specific_function, SpecificClass
Star imports pull an unpredictable set of names into your namespace, risk silently overwriting existing names, and make it impossible to tell where a given name came from just by reading the file.
Importing at module top level assumes the other module has already finished executing; deferring the import into a function delays the lookup until it's actually needed, after both modules are fully loaded.
What is the difference between import module and from module import name?
import module binds one name (the module object) and requires dotted access; from module import name copies a direct reference to that name into your namespace.
Why does importing the same module from ten different files only execute its top-level code once?
Because Python registers every loaded module in sys.modules; subsequent imports of the same name are cache lookups, not re-executions.
What is sys.path, and what happens if a module isn't found anywhere on it?
sys.path is the ordered list of locations Python searches for a module; if the name isn't found in any of them, Python raises ModuleNotFoundError.
What's the practical difference between ModuleNotFoundError and ImportError (cannot import name)?
ModuleNotFoundError means the module file itself wasn't found; ImportError with 'cannot import name' means the module was found but the specific requested name doesn't exist in it yet or at all.
Why can a circular import sometimes work and sometimes fail depending on where in each file the import happens?
Because a module is registered in sys.modules and starts executing top to bottom before it finishes; a name defined later in the file isn't available yet if the other module in the cycle tries to access it mid-load.
Why is from module import * generally discouraged?
It imports an unpredictable, potentially large set of names, can silently overwrite existing names in your namespace, and hides where any given name actually came from.
Write code that imports the standard library sys module and prints whether 'os' is currently in sys.modules before and after importing os.
import sys print('os' in sys.modules) import os print('os' in sys.modules)
The first print is typically False (unless something else already imported os); after import os runs, the second print is True, because the import registers the module in sys.modules.
What does this print, and why?
True. Both import json and import json as j resolve to the exact same cached module object in sys.modules; the alias only changes the local name bound to it, not which object is loaded.
Explain why from mypackage import helper_function can raise ImportError even though mypackage.py exists and has no syntax errors.
ImportError with 'cannot import name' means the module was found and loaded, but the requested name isn't defined in its namespace — either because of a typo/renamed function, or because the module is only partially initialized due to a circular import at the point the name is looked up.
import sys
import json
import json as j
print(sys.modules['json'] is j)