Skip to content

Imports, Namespaces, and Python Runtime

Step 1 of 41

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:

  1. Write import x, import x as y, from x import a, from x import a as b and from x import a, b and say what name each one binds.
  2. Predict whether a later line raises NameError, AttributeError or runs fine.
  3. Explain what a namespace is and name the four lookup layers (local, enclosing, global, built-in).
  4. Use dir() with and without an argument and interpret the result.
  5. Identify shadowing caused by star imports, by reassignment and by local file names.
  6. 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:

  1. Local: names bound inside the current function.
  2. Enclosing: names in any outer function (relevant to closures, covered later).
  3. Global: names bound at the top level of the current module (the file).
  4. Built-in: names such as print, len, round and sum, 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 where ceil came from.
  • Silent overwriting. If your script already defined pi = 3, the star import replaces it. If you define pi = 3 after 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 = 5
  • ceil(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 y removes x from view. Using x afterwards raises NameError.
  • Pitfall: Confusing NameError (the name is not bound) with AttributeError (the name is bound, but the object has no such attribute) and with ImportError (the import itself failed).
  • Pitfall: Believing from math import sqrt also lets you write math.pi. It does not; math was never bound.
  • Pitfall: Overlooking that a function def or a class class statement 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 math just 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.

  1. What are the values printed?
  2. Which round is used in the final line?
  3. How would you rewrite the code to remove the risk?

Solution.

  1. The helper round is a global name, so it comes before the built-in round in the LEGB lookup order. Calls: ceil(1.2) = 2, ceil(4.0) = 4, ceil(7.5) = 8, so total = 14. The first output is 14.
  2. The final line calls the helper, not the built-in: ceil(2.5) = 3. The built-in round(2.5) would have returned 2 because of the banker's rounding rule, so the two functions give different answers. Output: 3.
  3. Replace the star import with from math import ceil, rename the helper to round_up_kg, and keep the built-in round available. The new code states clearly where each name comes from, and a future star import cannot change the meaning of ceil or round silently. 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, class or import.
  • 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 y and from x import a as b do not bind x or a.
  • I can distinguish ModuleNotFoundError, ImportError, AttributeError and NameError from 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) and dir() to inspect names.
  • I can justify why star imports are discouraged.

Official sources