🎯 Learning Objectives
- Understand what a module is and why modules exist
- Use all five import styles and know when to pick each one
- Navigate Python's Standard Library with confidence
- Create your own modules and packages
- Understand how Python finds and loads modules
- Use the
__name__ == "__main__"guard correctly
What is a Module?
A module is simply a .py file. Any Python file you create
is a module — you can import its functions, classes, and variables into other files.
Modules let you split a large program into smaller, reusable, maintainable pieces. Instead of putting everything in one giant file, each module owns a focused area of responsibility.
# math_utils.py ← this file IS a module
def add(a, b):
return a + b
def square(n):
return n ** 2
PI = 3.14159
math_utils.py
# main.py ← import and use the module
import math_utils
print(math_utils.add(3, 4)) # 7
print(math_utils.square(5)) # 25
print(math_utils.PI) # 3.14159
main.py
Import Styles
There are five ways to import. Each has its place:
# 1. Import the whole module — access names via the module
import math
print(math.sqrt(16)) # 4.0
print(math.pi) # 3.141592653589793
# 2. Import the whole module under an alias
import math as m
print(m.sqrt(9)) # 3.0
# 3. Import specific names — use them directly
from math import sqrt, pi
print(sqrt(25)) # 5.0
print(pi) # 3.141592653589793
# 4. Import a specific name under an alias
from math import sqrt as sq
print(sq(36)) # 6.0
# 5. Import everything (wildcard) — avoid in most cases!
from math import *
print(floor(3.7)) # 3 — works but pollutes namespace
import_styles.py
| Style | Best for | Avoid when |
|---|---|---|
import module |
Standard, always safe, makes origin clear | Module name is very long (use alias) |
import module as alias |
Long names (numpy as np), name conflicts |
Alias obscures what the module is |
from module import name |
One or two specific items used frequently | Many names (clutters scope) |
from module import name as alias |
Resolving name clashes | Unnecessary renaming that harms clarity |
from module import * |
REPL/interactive exploration only | Production code — always avoid |
from module import * silently overwrites any existing names that share
the same identifier. It also makes it impossible to tell where a name came from when
reading the code later.
How Import Works
When Python encounters import math it performs three steps:
- Find — search
sys.pathfor a file calledmath.py(or a package/built-in) - Compile — compile the source to bytecode (
.pyccached in__pycache__/) - Execute — run the module's top-level code once and store the result in
sys.modules
Subsequent imports are free — Python returns the cached object from
sys.modules without re-running the file.
import sys
# See the search path Python uses to find modules
print(sys.path)
# ['', '/usr/lib/python3.11', '/usr/lib/python3.11/lib-dynload', ...]
# '' means the current directory — always checked first
# See all already-loaded modules
import math
print("math" in sys.modules) # True
# Both names below point to the SAME cached module object
import math as m1
import math as m2
print(m1 is m2) # True
sys_path.py
sys.path is a plain Python list — you can append to it at runtime to
teach Python about non-standard locations, though this is rarely needed in well-structured
projects.
Standard Library Highlights
Python ships with a huge standard library — "batteries included". Here are the modules you'll reach for most often:
| Module | Purpose | Key names |
|---|---|---|
os | OS interaction (files, env vars, processes) | os.getcwd(), os.listdir(), os.environ |
sys | Interpreter info & control | sys.argv, sys.path, sys.exit() |
pathlib | Object-oriented file paths | Path("dir/file.txt"), .read_text() |
math | Maths functions & constants | sqrt(), ceil(), pi, inf |
datetime | Dates, times, timedeltas | datetime.now(), date.today() |
json | Encode/decode JSON | json.loads(), json.dumps() |
random | Pseudo-random numbers & choices | random(), choice(), shuffle() |
collections | Specialised containers | Counter, defaultdict, deque |
itertools | Iterator building blocks | chain(), product(), groupby() |
re | Regular expressions | re.search(), re.findall() |
string | String constants & helpers | string.ascii_letters, Template |
functools | Higher-order function tools | partial(), lru_cache(), reduce() |
import os
import math
from datetime import datetime
from collections import Counter
import random
# os — current working directory and environment
print(os.getcwd()) # /home/user/project
print(os.environ.get("HOME")) # /home/user
# math
print(math.sqrt(144)) # 12.0
print(math.ceil(4.1)) # 5
print(math.factorial(5)) # 120
# datetime
now = datetime.now()
print(now.strftime("%Y-%m-%d %H:%M")) # 2024-03-15 10:30
# Counter — count elements in an iterable
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
freq = Counter(words)
print(freq) # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
print(freq.most_common(2)) # [('apple', 3), ('banana', 2)]
# random
print(random.randint(1, 10)) # random int between 1 and 10 inclusive
print(random.choice(["a", "b", "c"])) # random element
stdlib_tour.py
Creating Your Own Modules
Any .py file is importable as a module. The filename (without
.py) becomes the module name.
# greetings.py
"""Utility functions for generating greeting messages."""
DEFAULT_LANG = "en"
_GREETINGS = { # underscore prefix = "private by convention"
"en": "Hello",
"es": "Hola",
"fr": "Bonjour",
"de": "Hallo",
}
def greet(name, lang=DEFAULT_LANG):
"""Return a greeting string for the given name and language."""
word = _GREETINGS.get(lang, _GREETINGS["en"])
return f"{word}, {name}!"
def greet_all(names, lang=DEFAULT_LANG):
"""Return a list of greetings for each name."""
return [greet(n, lang) for n in names]
greetings.py
# app.py — importing our custom module
from greetings import greet, greet_all
print(greet("Alice")) # Hello, Alice!
print(greet("Carlos", "es")) # Hola, Carlos!
print(greet_all(["Ana", "Bob"])) # ['Hello, Ana!', 'Hello, Bob!']
app.py
module.__doc__ and is shown by
help(module).
Private names by convention
A name starting with a single underscore (_name) signals
"internal — don't import this directly". Python won't enforce it,
but from module import * will skip underscored names,
and IDEs will warn about them.
import greetings
# Public API — intended for import
print(greetings.greet("Alice"))
# Private by convention — accessible, but shouldn't be used directly
print(greetings._GREETINGS) # works, but signals "internal"
private_names.py
Packages & __init__.py
A package is a directory containing Python modules.
A directory becomes a package when it contains an __init__.py file
(which can be empty).
myapp/
├── main.py
└── utils/
├── __init__.py ← marks utils/ as a package
├── text.py
└── numbers.py
project structure
# utils/text.py
def capitalise_words(sentence):
return " ".join(w.capitalize() for w in sentence.split())
# utils/numbers.py
def is_even(n):
return n % 2 == 0
# utils/__init__.py — can re-export for convenience
from .text import capitalise_words
from .numbers import is_even
utils/
# main.py — various ways to import from the package
import utils # uses __init__.py exports
from utils import capitalise_words
from utils.text import capitalise_words
from utils.numbers import is_even
print(capitalise_words("hello world")) # Hello World
print(is_even(4)) # True
main.py
__init__.py
for the directory to be importable. But adding __init__.py is still best
practice — it makes intent clear and enables the re-export pattern above.
The __name__ == "__main__" Guard
When Python runs a file directly (python myfile.py),
it sets __name__ to "__main__".
When the same file is imported as a module, __name__
is set to the module name instead.
This lets you write code that runs only when executed directly — not when imported:
# calculator.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
# ─── This block only runs when you do: python calculator.py ───
if __name__ == "__main__":
print("Running calculator in standalone mode")
print(add(10, 5)) # 15
print(subtract(10, 5)) # 5
calculator.py
# other.py — importing calculator doesn't trigger the if-block
from calculator import add
result = add(3, 7)
print(result) # 10 (no "Running calculator..." message)
other.py
if __name__ == "__main__".
It makes every Python file both a runnable script AND a reusable module.
Without the guard, importing the file would execute all its top-level code —
a frequent source of side-effect bugs.
Relative vs Absolute Imports
Inside a package you can use either absolute imports (full path from the project root) or relative imports (path relative to the current module).
# Absolute imports — always work, always clear
from utils.text import capitalise_words
from utils.numbers import is_even
# Relative imports — use dots to mean "here" or "parent"
from .text import capitalise_words # . = same package
from .numbers import is_even
from ..models import User # .. = one package up
relative_vs_absolute.py
Common Pitfalls
Circular imports
# a.py
from b import func_b # b imports from a → ImportError!
# b.py
from a import func_a # creates a circular dependency
circular.py
Fix circular imports by restructuring: extract shared code into a third module
that both a and b import from, instead of importing
each other.
Shadowing a standard-library module
# ❌ Don't name your files after stdlib modules
# If you create random.py, json.py, os.py, etc. in your project,
# Python will find YOUR file first (because '' is first in sys.path)
# and stdlib imports will silently break.
# random.py ← BAD name if stdlib random is also needed
import random # imports YOUR random.py, not stdlib!
shadowing.py
Wildcard import hides name origins
from os.path import *
from pathlib import *
# Now: where does 'join' come from? os.path? pathlib? Your own code?
# Impossible to tell without reading both modules.
path = join("home", "user") # os.path.join — but not obvious!
wildcard_problem.py
Module-level code with side effects
# ❌ Side effects at import time surprise callers
# bad_module.py
print("Connecting to database...") # runs on every import!
db = connect("production-db") # also runs on import
# ✓ Guard side effects or put them in functions / __name__ guard
# good_module.py
def get_db():
return connect("production-db") # only connects when called
side_effects.py
Reloading a Module
Because Python caches modules in sys.modules, changes to a file
don't take effect until you restart the interpreter — or explicitly reload:
import importlib
import my_module
# Edit my_module.py here...
importlib.reload(my_module) # re-runs the file and updates the cached object
reload.py
importlib.reload() is mainly useful in the REPL or Jupyter notebooks
during development. In production code, prefer restarting the process rather than
reloading modules — reload has subtle edge cases with references held before reload.
Best Practices
- One import per line for clarity and clean diffs.
- Group imports in PEP 8 order: standard library → third-party → local. Separate groups with a blank line.
- Prefer
import moduleoverfrom module import *. - Use
__name__ == "__main__"in every script that also serves as a module. - Avoid name collisions — don't name your files after stdlib modules.
- Keep module-level code side-effect-free.
- Document your public API with a module docstring and
__all__if needed.
# ✓ Ideal import section of a file (PEP 8 order)
# 1. Standard library
import os
import sys
from pathlib import Path
from datetime import datetime
# 2. Third-party (installed via pip)
import requests
import numpy as np
# 3. Local / project modules
from myapp.utils.text import capitalise_words
from myapp.models import User
import_order.py
Controlling the public API with __all__
# greetings.py — explicitly declare what's public
__all__ = ["greet", "greet_all"] # only these are exported by 'from greetings import *'
DEFAULT_LANG = "en"
_GREETINGS = {"en": "Hello", "es": "Hola"}
def greet(name, lang=DEFAULT_LANG):
return f"{_GREETINGS.get(lang, 'Hello')}, {name}!"
def greet_all(names, lang=DEFAULT_LANG):
return [greet(n, lang) for n in names]
def _internal_helper(): # not in __all__, skipped by wildcard import
pass
all_example.py
Primary sources: Python Docs — Modules · Python Standard Library · PEP 8 — Import Style Guide
Ask your AI tutor! Getting an ImportError or
ModuleNotFoundError? Not sure whether to use a relative or absolute
import? Building a package for the first time? These are great questions to ask.
💻 Exercises
Create a file string_utils.py with the following functions:
is_palindrome(s)— returnsTrueifsreads the same forwards and backwards (case-insensitive, ignore spaces)word_count(text)— returns a dict mapping each unique word to its frequencytruncate(text, max_len, suffix="…")— returns text trimmed tomax_lenchars withsuffixappended if truncated
Then write a main.py that imports and tests all three.
Show solution
# string_utils.py
"""String utility functions."""
def is_palindrome(s):
cleaned = s.replace(" ", "").lower()
return cleaned == cleaned[::-1]
def word_count(text):
counts = {}
for word in text.lower().split():
counts[word] = counts.get(word, 0) + 1
return counts
def truncate(text, max_len, suffix="…"):
if len(text) <= max_len:
return text
return text[:max_len - len(suffix)] + suffix
# main.py
from string_utils import is_palindrome, word_count, truncate
print(is_palindrome("racecar")) # True
print(is_palindrome("A man a plan")) # True
print(is_palindrome("hello")) # False
print(word_count("the cat sat on the mat"))
# {'the': 2, 'cat': 1, 'sat': 1, 'on': 1, 'mat': 1}
print(truncate("Hello, world!", 8)) # Hello, …
print(truncate("Hi", 10)) # Hi
Create a small package called geometry/ with three modules:
geometry/circle.py—area(r)andcircumference(r)geometry/rectangle.py—area(w, h)andperimeter(w, h)geometry/__init__.py— re-export all four functions for convenience
Import from the package in main.py using both styles:
from geometry import area # which area? — ambiguous! think about __init__.py design
from geometry.circle import area as circle_area
Show solution
# geometry/circle.py
import math
def area(r):
return math.pi * r ** 2
def circumference(r):
return 2 * math.pi * r
# geometry/rectangle.py
def area(w, h):
return w * h
def perimeter(w, h):
return 2 * (w + h)
# geometry/__init__.py
# Note: we can't re-export both 'area' functions with the same name.
# Instead expose them with namespaced aliases:
from .circle import area as circle_area, circumference
from .rectangle import area as rect_area, perimeter
# main.py
from geometry import circle_area, rect_area, circumference, perimeter
from geometry.circle import area as ca
print(circle_area(5)) # 78.539...
print(rect_area(4, 6)) # 24
print(circumference(5)) # 31.415...
print(perimeter(4, 6)) # 20
print(ca(5)) # 78.539...
Write a file stats.py that:
- Defines functions
mean(numbers),median(numbers), andmode(numbers) - When run directly (
python stats.py), runs a small demo using a hardcoded list of numbers and prints results - When imported, only exposes the three functions — no output is produced
Show solution
# stats.py
"""Basic descriptive statistics functions."""
from collections import Counter
def mean(numbers):
return sum(numbers) / len(numbers)
def median(numbers):
s = sorted(numbers)
n = len(s)
mid = n // 2
return s[mid] if n % 2 else (s[mid - 1] + s[mid]) / 2
def mode(numbers):
freq = Counter(numbers)
max_count = max(freq.values())
modes = [k for k, v in freq.items() if v == max_count]
return modes[0] if len(modes) == 1 else modes
if __name__ == "__main__":
data = [4, 7, 13, 2, 7, 1, 9, 7, 4, 2]
print(f"Data: {data}")
print(f"Mean: {mean(data):.2f}") # 5.60
print(f"Median: {median(data)}") # 5.5
print(f"Mode: {mode(data)}") # 7