Resolve Imports and Namespaces
Introduction
This lesson opens the Modules and Packages section of the PCAP-31-03 syllabus, which accounts for 12% of the score and 6 of the 40 items on the exam. Objective 1.1 asks you to import and use modules with import, from ... import ..., from ... import ... as ... and import ... as ..., to use the dir() function to inspect what a module provides, and to reason about namespaces. Almost every other PCAP topic, from the math and random modules to exceptions, strings and file handling, relies on you being fluent with imports, so this lesson is the foundation for the rest of the course.
The exam rarely asks you to recite a definition. It shows you a short script, sometimes with a deliberate collision between names, and asks what is printed or which line fails. To answer reliably you need a precise mental model of where a name lives when Python looks it up. By the end of this lesson you will be able to:
- predict the effect of each of the four import forms on the current namespace;
- explain why
from module import *is risky and what it does to existing names; - use aliases to keep qualified names short and to resolve collisions;
- inspect a module or the current scope with
dir(); - trace a name-shadowing bug to the exact line that created it.
If your Python is rusty, the short refresher below covers the only prerequisite ideas: assignment, functions and scope.
Learning objectives
After this lesson you can:
- Write
import x,import x as y,from x import a,from x import a as bandfrom x import a, band say what name each one binds. - Predict whether a later line raises
NameError,AttributeErroror runs fine. - Explain what a namespace is and name the four lookup layers (local, enclosing, global, built-in).
- Use
dir()with and without an argument and interpret the result. - Identify shadowing caused by star imports, by reassignment and by local file names.
- Choose the import style that is safest for a given situation.
Refresher: names, objects and scope
Python variables are names bound to objects. The statement price = 10 does not put 10 "inside" price; it makes the name price refer to an integer object. A namespace is the mapping from names to objects. There are several, and Python searches them in a fixed order, often summarized as LEGB:
- Local: names bound inside the current function.
- Enclosing: names in any outer function (relevant to closures, covered later).
- Global: names bound at the top level of the current module (the file).
- Built-in: names such as
print,len,roundandsum, supplied by the interpreter.
A name is looked up from the inside out, and the first match wins. This one rule explains most of the "tricky" import questions. An import statement is simply a special kind of name binding: it executes (or finds) a module and then binds one or more names in the current namespace.
total = 5 # binds 'total' in the global namespace
def show():
local_total = 7 # binds 'local_total' in show()'s local namespace
print(total, local_total)
show() # 5 7
Because each module has its own global namespace, a module is also a namespace container. When you import math, Python creates (once) a module object whose attributes, such as sqrt and pi, live in the math namespace. Your own namespace gets just one new name: math.
The four import forms
Form 1: import module
import math
print(math.sqrt(81)) # 9.0
print(sqrt(81)) # NameError: name 'sqrt' is not defined
After import math, exactly one new name, math, exists in your namespace. Everything else must be reached through it with the dot operator: math.sqrt, math.pi. The upside is clarity: any reader can see where sqrt comes from. The downside is verbosity. Nothing from the module leaks into your scope, so there is no collision risk.
You can import several modules in one statement, import math, random, but the common style is one per line.
Form 2: import module as alias
import math as m
print(m.floor(3.9)) # 3
print(math.floor(3.9)) # NameError: name 'math' is not defined
An alias renames the binding. After import math as m, the name math is not defined in your namespace; only m is. This is a favourite exam trap: the original name disappears from your scope. The alias form is useful when a module name is long (import statistics as stats) or when it collides with something you already have.
Form 3: from module import name
from math import sqrt, pi
print(sqrt(16)) # 4.0
print(pi) # 3.141592653589793
print(math.sqrt(16)) # NameError: name 'math' is not defined
Here the module itself is not bound. Only sqrt and pi are, directly in your namespace. You call sqrt(16) without a prefix. Importing an attribute that does not exist raises ImportError at the import line itself:
from math import square_root
# ImportError: cannot import name 'square_root' from 'math' (unknown location)
Compare that with import math followed by math.square_root(4), which fails later with AttributeError. The difference in when and which exception is worth memorizing:
| Statement | Fails with | When |
|---|---|---|
import nosuchmodule |
ModuleNotFoundError (a subclass of ImportError) |
at the import line |
from math import nosuchname |
ImportError |
at the import line |
math.nosuchname after import math |
AttributeError |
when that expression runs |
using sqrt after only import math |
NameError |
when that expression runs |
Form 4: from module import name as alias
from math import sqrt as root
print(root(49)) # 7.0
print(sqrt(49)) # NameError: name 'sqrt' is not defined
Again, only the alias is bound. The original attribute name sqrt is not created in your namespace. Aliasing individual names is the standard way to resolve a collision between two libraries that both export a function with the same name.
Importing everything: from module import *
from math import *
print(sqrt(25)) # 5.0
print(pi) # 3.141592653589793
The star form copies all public names of the module into your namespace in one go. (A module can limit what "all" means by defining a list called __all__; without it, every name that does not start with an underscore is imported.) It looks convenient, but it has real costs:
- Hidden provenance. A reader of
ceil(x)cannot tell whereceilcame from. - Silent overwriting. If your script already defined
pi = 3, the star import replaces it. If you definepi = 3after the star import, you overwrite the library's value. - Collisions between two star imports. The later import wins, and no error is raised.
- Fragile code. A future version of the library may add a new public name that collides with one of yours.
Style guides, including PEP 8, discourage star imports outside of narrow, deliberate cases. On the exam you must simply be able to predict what happens.
Summary of what each form binds
| Statement | Names added to your namespace |
|---|---|
import math |
math |
import math as m |
m (not math) |
from math import sqrt |
sqrt (not math) |
from math import sqrt as root |
root (not sqrt, not math) |
from math import * |
every public name of math (not math) |
Namespace collisions and shadowing
Shadowing happens when a new binding hides an older one that has the same name in the same or an outer scope. There are three routes to it that the exam likes.
Route 1: a definition or import replaces an earlier name
from math import ceil
def ceil(x): # same name, new binding
return int(x) + 1
print(ceil(4.0)) # 5, not 4
The def statement rebinds ceil, so the library function is no longer reachable by that name. The same happens in the other direction: a star import placed after your own definition replaces it if the module exports the same name. Python never warns about this. Always ask "which binding ran last?"
Route 2: reassignment after the import
import math
math = 10 # the name 'math' now refers to an integer
print(math.sqrt(4))
# AttributeError: 'int' object has no attribute 'sqrt'
The module object is still loaded, but your name no longer points to it. The error is an AttributeError, not a NameError, because the name math exists; it just refers to something that has no sqrt attribute.
Route 3: two imports of the same name
from math import sqrt
from cmath import sqrt # a different sqrt (complex numbers)
print(sqrt(-4)) # 2j
The second binding replaces the first. If the order were reversed, sqrt(-4) would raise ValueError: math domain error. This is why aliases exist:
from math import sqrt
from cmath import sqrt as csqrt
print(sqrt(4)) # 2.0
print(csqrt(-4)) # 2j
Inspecting namespaces with dir()
The built-in dir() function returns a sorted list of names.
dir(module)lists the attributes of a module object.dir()with no argument lists the names in the current scope.
import math
names = dir(math)
print('sqrt' in names) # True
print(names[:3]) # ['__doc__', '__loader__', '__name__']
Notice that the list includes "dunder" names such as __doc__ and __name__; every module has them. Since the list is sorted alphabetically with uppercase letters first, and underscores sort before letters, the dunder names appear at the beginning.
Calling dir() with no argument is useful to see the effect of an import:
before = set(dir())
import math
from random import choice
after = set(dir())
print(sorted(after - before)) # ['before', 'choice', 'math']
The set difference shows the two names bound by the import statements, plus before itself, which was assigned after the first dir() call ran. This trick lets you verify any claim in the summary table above, and it is an excellent way to practise.
You can also check whether a name exists using 'sqrt' in dir(math) or hasattr(math, 'sqrt').
Worked example: predicting a collision
Problem. A pricing script contains the following. What does it print, and why?
from math import ceil
def ceil(x):
return int(x) + 1
print(ceil(4.0))
print(ceil(4.2))
Step 1: list the bindings in order. The first line binds ceil to math.ceil.
Step 2: process the definition. The def statement binds the name ceil to the new function object, replacing math.ceil. A function definition is also a name binding, with the same rules as an assignment.
Step 3: evaluate the calls. Both calls use the local helper:
ceil(4.0):int(4.0) + 1 = 5ceil(4.2):int(4.2) + 1 = 5
Output:
5
5
Why this matters. The author expected math.ceil(4.0) to be 4. The helper is wrong for whole numbers, and the real function was hidden without any error message. Python does not warn you when a binding replaces another.
Why other approaches are better. If the script used import math and called math.ceil(...), the helper could have been named ceil without any conflict. If the helper really was needed, giving it a distinct name such as next_whole would avoid the problem entirely. Using from math import ceil as math_ceil would also work, but it makes the helper and the library function look equally important.
Worked example 2: tracing built-in shadowing
from math import floor
numbers = [3.7, 2.2, 9.9]
sum = floor(numbers[0]) + floor(numbers[1])
print(sum) # 5
print(sum(numbers)) # TypeError: 'int' object is not callable
The assignment sum = 5 creates a global name sum that sits in front of the built-in sum in the LEGB order. The second print calls the integer 5. The fix is to rename the variable (subtotal). If you really need the built-in again, del sum removes the global binding and the lookup falls back to the built-in layer. This is a classic exam situation because the first print works and the second does not.
Comparing the styles
| Style | Typical use | Advantage | Risk |
|---|---|---|---|
import module |
Most code | Clear origin of every name, no collisions | Longer lines |
import module as alias |
Long or conflicting module names | Short prefixes | Original name is not bound |
from module import name |
A few frequently used functions | Short calls | Name may collide with your own |
from module import name as alias |
Resolve collisions | Explicit and safe | Alias may confuse readers if unusual |
from module import * |
Interactive exploration only | Fast typing | Hidden origin, silent overwriting |
Common pitfalls and exam tips
- Exam tip: Whenever you see an import, write down the names it binds before reading further. Most questions are answered by that list.
- Pitfall: Forgetting that
import x as yremovesxfrom view. Usingxafterwards raisesNameError. - Pitfall: Confusing
NameError(the name is not bound) withAttributeError(the name is bound, but the object has no such attribute) and withImportError(the import itself failed). - Pitfall: Believing
from math import sqrtalso lets you writemath.pi. It does not;mathwas never bound. - Pitfall: Overlooking that a function
defor a classclassstatement is also a binding that can shadow imports and built-ins. - Exam tip: Read the order of statements. The last binding of a name before the line being evaluated determines its value.
- Exam tip: Importing a module twice is harmless. Python caches loaded modules, so the second
import mathjust binds the name again. - Pitfall: Assuming
dir()returns an unsorted list. It is sorted alphabetically. - Pitfall: Using a star import and then assuming a name comes from your own code when it may have been replaced by the module.
Mini case study: the shipping calculator
A small company wrote a shipping calculator in a single file.
from math import *
def round(value):
"""Round up to the next whole kilogram."""
return ceil(value)
weights = [1.2, 4.0, 7.5]
total = 0
for w in weights:
total += round(w)
print(total)
print(round(2.5))
A colleague asks you to explain what this code prints and whether it is safe.
Questions.
- What are the values printed?
- Which
roundis used in the final line? - How would you rewrite the code to remove the risk?
Solution.
- The helper
roundis a global name, so it comes before the built-inroundin the LEGB lookup order. Calls:ceil(1.2) = 2,ceil(4.0) = 4,ceil(7.5) = 8, sototal = 14. The first output is14. - The final line calls the helper, not the built-in:
ceil(2.5) = 3. The built-inround(2.5)would have returned2because of the banker's rounding rule, so the two functions give different answers. Output:3. - Replace the star import with
from math import ceil, rename the helper toround_up_kg, and keep the built-inroundavailable. The new code states clearly where each name comes from, and a future star import cannot change the meaning ofceilorroundsilently. The ordering problem also disappears because no name is defined twice.
The lesson of the case: names are looked up at call time, and the most recent binding wins. The cost of a convenient shortcut appears months later, in a different file.
Key terms
- Namespace: a mapping from names to objects (module global scope, function local scope, built-in scope).
- Binding: the association of a name with an object, created by assignment,
def,classorimport. - Module: a file of Python code, loaded once and exposed as an object whose attributes are its names.
- Alias: an alternative name given with
as. - Qualified name: a name used with its container, such as
math.sqrt. - Shadowing: a binding that hides another one of the same name.
- LEGB: the lookup order Local, Enclosing, Global, Built-in.
dir(): built-in returning the sorted list of names of an object or of the current scope.- Star import:
from module import *. __all__: an optional list in a module that defines which names a star import copies.
Summary checklist
- I can state which names each of the four import forms (and the star form) binds.
- I know that
import x as yandfrom x import a as bdo not bindxora. - I can distinguish
ModuleNotFoundError,ImportError,AttributeErrorandNameErrorfrom a short snippet. - I can explain LEGB and use it to find the value of a name.
- I can predict the result of a collision between two imports or an import and a definition.
- I can use
dir(module)anddir()to inspect names. - I can justify why star imports are discouraged.
Official sources
- Python Institute, PCAP certification page: https://pythoninstitute.org/pcap (opens in a new tab)
- PCAP-31-0x exam syllabus (objective 1.1): https://pythoninstitute.org/pcap-exam-syllabus (opens in a new tab)
- Python documentation, the import system: https://docs.python.org/3/reference/import.html (opens in a new tab)
- Python documentation,
dir(): https://docs.python.org/3/library/functions.html#dir (opens in a new tab)
