PYTHON / MODULES AND PACKAGES
import, from-import, and module objects
Explain what import actually does, treat modules as ordinary objects living in sys.modules, and choose between `import x` and `from x import y` deliberately.
What you will learn
- Read `import x` as: load once, cache in sys.modules, bind the module object to a name
- Predict when `from x import y` leaves you holding a stale value after `x.y` is rebound
- Inspect any module through __name__, __file__, __dict__ and dir()
- Know that `as` renames only your local binding, not the module's identity
Understanding import, from-import, and module objects
An `import` statement does three separate things. It locates the module and executes its top-level code exactly once, it wraps the resulting namespace in an object of type `module` and files that object in the dictionary `sys.modules` under the module's dotted name, and finally it binds a name in the importing namespace. That is the whole mental model: `math.sqrt` is not special syntax, it is an attribute lookup in the `math` module object's `__dict__`, which is why `math.__dict__['sqrt'] is math.sqrt` holds.
`from math import sqrt` performs identical loading work — the entire module is still executed and cached — and then pulls one entry out of the module's namespace and binds it in yours. The consequence is that you now have two independent names pointing at the same object. If someone later rebinds `math.sqrt`, your `sqrt` keeps referring to the old object, because assignment replaces a dictionary entry rather than mutating what the entry pointed to. This is exactly why `import config` plus `config.DEBUG` tracks changes while `from config import DEBUG` freezes a snapshot taken at import time.
Because `sys.modules` is checked before anything else, the second and every later `import` of the same module is just a dict lookup, so module bodies run once per process, not once per import statement. The `as` clause only changes which local name you get; the module's `__name__` and its `sys.modules` key are unaffected, so `import math as m` gives `m is math` once both have been executed. Dotted imports follow one extra rule: `import xml.etree.ElementTree` imports every package on the path and binds only the top name `xml`, reaching the rest through attributes, while `import xml.etree.ElementTree as ET` binds `ET` alone.
import math
from math import sqrt
import sys
print(type(math))
print(math.__name__)
print(sqrt is math.sqrt)
print(sqrt is math.__dict__['sqrt'])
print(sys.modules['math'] is math)
import math as m
print(m is math, m.__name__)An import loads a module once into a module object stored in sys.modules; `import` binds that object, while `from`-import copies references out of it.
Worked examples
A module is just an object
Builds a module object by hand, registers it in sys.modules, and shows that from-import takes a snapshot.
import sys
import types
mod = types.ModuleType("greet")
mod.name = "world"
mod.hello = lambda: "hello, " + mod.name
sys.modules["greet"] = mod
import greet
from greet import name
print(greet is mod)
print(greet.__name__, name, greet.hello())
greet.name = "python"
print(name, greet.hello())Example explained
Line 1`types.ModuleType("greet")` creates the same kind of object the import machinery would build; no file exists anywhere.
Line 2`import greet` succeeds because the import system checks `sys.modules` first and finds the entry we planted.
Line 3`from greet import name` binds a second reference to the string "world"; `greet.name = "python"` only replaces the module's dict entry.
Line 4`greet.hello()` reads `mod.name` at call time, so it reports the new value while the plain `name` still shows the old one.
The body runs once, not once per import
Creates a real module file on disk and imports it twice to show the sys.modules cache in action.
import pathlib
import sys
import tempfile
d = tempfile.mkdtemp()
pathlib.Path(d, "noisy.py").write_text("print('running noisy top level')\nVALUE = 42\n")
sys.path.insert(0, d)
import noisy
import noisy
from noisy import VALUE
print(VALUE, noisy.__name__)
print(noisy.__file__.endswith("noisy.py"))Example explained
Line 1Prepending the temp directory to `sys.path` is what makes the name `noisy` findable at all.
Line 2The first `import noisy` executes the file top to bottom, which is why the message appears exactly once.
Line 3The second `import noisy` and the `from noisy import VALUE` are both `sys.modules` hits, so no code re-runs.
Line 4`__file__` and `__name__` are attributes the loader set on the module object during that single execution.
Dotted names and aliases
Shows which name a dotted import actually binds and that the alias form does not bind the package.
import sys
import xml.etree.ElementTree as ET
print(ET.__name__)
print("xml" in globals())
import xml.etree.ElementTree
print("xml" in globals())
print(xml.etree.ElementTree is ET)
print(sys.modules["xml.etree"] is xml.etree)Example explained
Line 1`import a.b.c as x` binds only `x`, so `xml` is absent from globals even though the package was loaded.
Line 2`ET.__name__` is still the full dotted path: the alias renamed your variable, not the module.
Line 3The plain `import xml.etree.ElementTree` binds just the top name `xml`; the rest is reached by attribute access.
Line 4Each package level gets its own `sys.modules` key, and submodules are stored as attributes of their parent.
Important notes
Re-executing `import mod` after editing mod.py changes nothing, because the cached object in sys.modules is returned untouched; reload it or restart the process.
`from mod import *` copies mod's public names (or exactly its `__all__`) into your namespace and erases any trace of where each name came from, so keep it out of library code.
Common mistakes
Believing `from big_module import one_function` skips the rest of the file: the whole top-level body still executes, including its own imports and side effects, so import time is unchanged.
Using `from settings import DEBUG` and then setting `settings.DEBUG = True` elsewhere: your local `DEBUG` still points at the old object, producing silent stale behaviour that is hard to trace.
Writing `import mypkg.helpers.clean_name` when `clean_name` is a function: `import` only accepts module paths, so you get ModuleNotFoundError; use `from mypkg.helpers import clean_name`.
Try it yourself
Change, predict, then run
Build a module object with `types.ModuleType("settings")`, give it `DEBUG = False`, register it in `sys.modules`, then do `import settings` and `from settings import DEBUG`; set `settings.DEBUG = True` and print both `DEBUG` and `settings.DEBUG` to show the two bindings diverge.
Open the Python workspaceCheck your understanding
counter.py contains `total = 0` and `def bump(): global total; total += 1`. A script runs `from counter import total, bump`, calls `bump()` twice, then prints `total`. What does it print?
- 0, because the script's `total` is a separate name bound to the original int, and `bump` only rebinds `counter.total`
- 2, because `total` in the script and `total` in counter are the same variable
- 2, because integers become mutable once they are reached through a module namespace
- It raises NameError, because `global total` removes `total` from the module namespace
Show answer
`from`-import copies the reference that existed at import time into the script's namespace. `total += 1` inside `bump` rebinds the entry in counter's namespace to a new int object, leaving the script's name pointing at 0. Option 2 is tempting because it feels like a shared variable, but Python has no aliasing of names across namespaces; you would need `import counter` and read `counter.total` to see 2.