1.3.1 · D4Python Intermediate

Exercises — Modules — import, from…import, as aliasing

2,433 words11 min readBack to topic

Before we start, one picture of the whole idea: importing is just binding a name in your file that points at code living somewhere else.

Figure — Modules — import, from…import, as aliasing

L1 — Recognition

These check that you can read an import line and say what name it creates.

Exercise 1.1

For each line, state the single name (or names) that becomes usable in your file, and whether a dot prefix is required to reach the contents.

import math                      # (a)
from math import sqrt            # (b)
import numpy as np               # (c)
from datetime import datetime as dt   # (d)
Recall Solution 1.1

Ask for each: what label did this bind, and is the stuff behind a dot?

  • (a) import math → binds the name math. Contents are behind the dot: math.pi, math.sqrt.
  • (b) from math import sqrt → binds the name sqrt directly (no dot). You cannot write math.sqrt — the name math was never bound here.
  • (c) import numpy as np → binds np (not numpy). Contents behind the dot: np.array. The name numpy is not bound.
  • (d) from datetime import datetime as dt → binds dt directly (no dot). dt.now() works; datetime.now() does not (the name datetime was never bound).

Exercise 1.2

Which of these lines makes the name pi usable without any prefix?

import math          # (a)
from math import pi  # (b)
import math as m     # (c)
Recall Solution 1.2

Only (b). from math import pi copies the name pi straight into your file, so you write pi.

  • (a) gives math.pi (dot needed).
  • (c) gives m.pi (dot needed, using the alias m).

L2 — Application

Now you run the code in your head and produce output.

Exercise 2.1

What does this print?

import math
r = 3
area = math.pi * math.pow(r, 2)
print(round(area, 2))
Recall Solution 2.1
  • math.pi .
  • math.pow(3, 2) (a float).
  • Area
  • round(area, 2) .

Output: 28.27. Note round needs no import — it's a built-in always present.

Exercise 2.2

What does this print?

from statistics import mean, median
data = [3, 1, 4, 1, 5, 9, 2]
print(round(mean(data), 3))
print(median(data))
Recall Solution 2.2
  • Sum , count , so mean .
  • median: sort , middle of 7 values is the 4th .

Output:

3.571
3

Exercise 2.3

What does this print, and why does the second import stay silent?

# mymod.py
print("loading mymod")
x = 10
 
# main.py
import mymod
import mymod
print(mymod.x + 5)
Recall Solution 2.3
  • First import mymod runs the file → prints loading mymod, sets x = 10.
  • Second import mymod finds mymod already in sys.modulesruns nothing.
  • mymod.x + 5 .

Output:

loading mymod
15

The top-level print fires exactly once — that's the caching guarantee. See sys and os modules for sys.modules.


L3 — Analysis

Here the code is legal but the naming interacts in tricky ways. Predict the result and explain the mechanism.

Exercise 3.1

Predict the output and name the bug's category.

from math import sqrt
 
def sqrt(n):
    return "I am not real math!"
 
print(sqrt(16))
Recall Solution 3.1

from math import sqrt copied the real sqrt into your namespace. Then def sqrt(...) rebinds the same name sqrt to your function. The last binding wins.

Output: I am not real math!

This is a silent shadowing bug: no error, wrong behaviour. With import math you'd call math.sqrt(16) and your sqrt couldn't collide. See Namespaces and Scope.

Exercise 3.2

You create a file random.py in your project folder containing just print("my file!"). In the same folder you run main.py:

# main.py
import random
print(random.randint(1, 6))

What happens, and why?

Recall Solution 3.2

When Python resolves import random, it walks sys.path in order, and entry #1 is the script's own directory. Your random.py is found first, shadowing the standard-library random.

  • So import random runs your file → prints my file!.
  • Then random.randintAttributeError: your file has no randint.

Fix: rename your file (never shadow a stdlib name). This is the sys.path "first match wins" rule from the parent note.

Exercise 3.3

Order these sys.path locations by search priority (first checked → last), then say which foo wins:

  • (P) the standard library folder,
  • (Q) the directory of the running script,
  • (R) directories listed in the PYTHONPATH environment variable. A module foo exists in both (Q) and (P).
Recall Solution 3.3

Search order: Q → R → P (script dir, then PYTHONPATH, then stdlib/installed). Since foo lives in both Q and P, and Q is checked first, the script-directory foo wins. The stdlib copy is never reached — exactly the mechanism behind Exercise 3.2.


L4 — Synthesis

Combine forms, aliases, and the once-only rule to build something correct.

Exercise 4.1

Rewrite this messy block so that (i) no dot prefix clutters the date math, (ii) the class name doesn't collide with its module, and (iii) timedelta keeps its clear name. Then state what tomorrow.date() conceptually returns.

import datetime
tomorrow = datetime.datetime.now() + datetime.timedelta(days=1)
Recall Solution 4.1

The pain is datetime.datetime — the module datetime contains a class also named datetime. Alias the class, import timedelta plainly:

from datetime import datetime as dt, timedelta
tomorrow = dt.now() + timedelta(days=1)
print(tomorrow.date())
  • (i) No datetime. noise — names copied in directly.
  • (ii) datetime as dt removes the class/module name clash.
  • (iii) timedelta is already unique and clear → left un-aliased.

tomorrow.date() returns the calendar date one day after now (a date object, time stripped off).

Exercise 4.2

A teammate wrote from math import * at the top of a 500-line file. Later, deep in the file, someone did gamma = 0.5. Now print(gamma) gives 0.5 but a call elsewhere gamma(5) raises TypeError: 'float' object is not callable. Explain the whole chain, then give the two safe rewrites.

Recall Solution 4.2

Chain of events:

  1. from math import * dumped every public name of math into the file — including the function math.gamma.
  2. gamma = 0.5 rebound that same name gamma to a float (silent shadowing — no warning).
  3. gamma(5) now tries to call a floatTypeError.

You can't even tell gamma came from math by reading the code — that's the readability cost of *.

Two safe rewrites:

from math import gamma          # explicit — you see exactly what you took
# ...but still avoid reusing the name 'gamma' for your variable.

or

import math                     # prefixed — math.gamma stays safe behind the dot
my_gamma = 0.5                  # your own name can never clash with math.gamma

This is the DRY Principle served without the star's mess.


L5 — Mastery

Everything at once: caching, namespaces, search order, and reasoning about a subtle multi-import scenario.

Exercise 5.1

Predict the complete output, in order.

# greet.py
print("greet.py top-level runs")
message = "hi"
 
def shout():
    return message.upper()
 
# main.py
import greet
from greet import shout, message
message = "bye"
print(greet.message)
print(message)
print(shout())
Recall Solution 5.1

Step by step, tracking each name:

  1. import greet → runs greet.py once → prints greet.py top-level runs. Binds name greet. Inside it, greet.message == "hi".
  2. from greet import shout, messagedoes not re-run the file (already cached). Copies two names into main: shout (the function) and message (currently "hi").
  3. message = "bye" → rebinds main's message to "bye". This does not touch greet.message — they are separate names in separate namespaces.
  4. print(greet.message) → still "hi" (greet's own copy, untouched).
  5. print(message)"bye" (main's copy).
  6. print(shout())shout reads message from greet's namespace (where it was defined), which is still "hi" → returns "HI".

Output:

greet.py top-level runs
hi
bye
HI

Key insight: from greet import message made a snapshot copy of the name — later reassigning message in main never propagates back, and shout() always sees its home namespace. This is the heart of Namespaces and Scope.

Exercise 5.2

Two files import each other's names. Given the caching rule, does this loop forever?

# a.py
print("a runs")
import b
A = 1
 
# b.py
print("b runs")
import a
B = 2
 
# main.py
import a

State the print order (assume main.py is run).

Recall Solution 5.2

The sys.modules cache is what stops the loop. Trace it:

  1. main does import a → Python registers a in sys.modules (empty so far), starts running a.py.
  2. a.py prints a runs, then hits import b → registers b, starts running b.py.
  3. b.py prints b runs, then hits import aa is already in sys.modules (half-built) → import returns immediately, no re-run.
  4. b.py finishes (B = 2), control returns to a.py.
  5. a.py finishes (A = 1).

Output:

a runs
b runs

Each file's top-level runs exactly once; the cache breaks the circular import. (Referencing a name that isn't defined yet during the half-built window can still fail — but here only the imports happen early, so it's safe.)


Recall One-line summary of every level

L1 recognise which name binds ::: import XX (dot), from X import yy (no dot), as ZZ only. L2 the once-only rule ::: repeated import re-runs nothing — cached in sys.modules. L3 shadowing & search order ::: script dir wins on sys.path; a redefinition overwrites an imported name silently. L4 clean composition ::: alias to kill clashes, list names explicitly, never import * in real code. L5 namespaces are separate copies ::: from X import y snapshots the name; reassigning yours never edits X's.


Connections

  • ← Back to the topic note
  • Namespaces and Scope — why copied names and prefixed names never collide.
  • sys and os modulessys.path search order and the sys.modules cache.
  • Packages and __init__.py — how circular imports get restructured.
  • DRY Principle — the reason all this reuse machinery exists.
  • Python Functions — what modules mostly bundle up.