Modules and the import Statement
A module is a .py file. import runs it once, caches it, and binds a name. Every import form is a variation on those three steps.
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
A module is just a file
# File: geometry.py
PI = 3.14159
def circle_area(radius):
return PI * radius ** 2
def rectangle_area(width, height):
return width * height
print("geometry module loaded")# File: main.py
import geometry
print(geometry.circle_area(2)) # 12.56636
print(geometry.PI) # 3.14159geometry module loaded
12.56636
3.14159Notice that "geometry module loaded" printed. Importing a module executes every top level statement in it. That is how the functions and variables come into existence, and it is why a module should not do real work at import time.
The import forms
import geometry # the whole module
geometry.circle_area(2)
import geometry as geo # with an alias
geo.circle_area(2)
from geometry import circle_area # one name, directly
circle_area(2)
from geometry import circle_area, PI # several names
from geometry import circle_area as area
from geometry import * # everything - avoid this| Form | You write | Use when |
|---|---|---|
import m | m.thing | Default. The origin of every name stays visible. |
import m as x | x.thing | The name is long or conventionally abbreviated. |
from m import thing | thing | You use one or two names heavily. |
from m import * | thing | Almost never. |
Why star imports are discouraged
from math import *
from statistics import *
# Which module did this come from? Both define it.
print(sqrt(16))
# And a real collision:
from os import *
open("file.txt") # os.open, not the built-in open - different behaviour entirelyA star import fills your namespace with names you did not choose and cannot see. It silently shadows built ins, breaks editor navigation and makes a later collision very hard to diagnose.
import runs a module only once
import geometry # prints "geometry module loaded"
import geometry # prints nothing
import geometry # prints nothing
import sys
print("geometry" in sys.modules) # TruePython keeps a cache in sys.modules. The first import executes the file; every later one just binds the name again. This is why importing the same module from ten different files costs nothing.
import importlib
import geometry
importlib.reload(geometry) # force a re-run, useful only in a REPL sessionWhere Python looks
import sys
for entry in sys.path:
print(entry)' the directory of the script being run
/usr/lib/python312.zip
/usr/lib/python3.12
/usr/lib/python3.12/lib-dynload
/usr/lib/python3.12/site-packages installed packagesPython searches these directories in order and stops at the first match. The script's own directory comes first, which is the cause of the classic shadowing bug:
# If your own file is called random.py:
import random
print(random.randint(1, 6)) # AttributeError: module has no attribute 'randint'Your random.py was found before the standard library one. The fix is to rename your file and delete the stale __pycache__. Never name a file after a module you intend to import: random.py, json.py, math.py, string.py, time.py, types.py, test.py, email.py, logging.py.
Writing a good module
# File: invoices.py
"""Helpers for building and totalling invoices."""
from decimal import Decimal
TAX_RATE = Decimal("0.18")
__all__ = ["line_total", "invoice_total"] # the public surface
def _round_money(value):
"""Internal helper. The leading underscore marks it as private."""
return value.quantize(Decimal("0.01"))
def line_total(price, quantity):
"""Return the total for one invoice line, including tax."""
subtotal = Decimal(str(price)) * quantity
return _round_money(subtotal * (1 + TAX_RATE))
def invoice_total(lines):
"""Return the total for a list of (price, quantity) pairs."""
return _round_money(sum(line_total(p, q) for p, q in lines))- A module docstring on the first line.
- Imports at the top, standard library first.
- Constants in capitals near the top.
- A leading underscore on anything internal.
__all__listing the public names, which is whatfrom module import *would export.- No code that runs at import time other than definitions.
Inspecting a module
import math
print(math.__name__) # math
print(math.__doc__[:60]) # its docstring
print([n for n in dir(math) if not n.startswith("_")][:10])
print(math.__file__ if hasattr(math, "__file__") else "built in")
help(math.gcd)Import placement
"""Module docstring first."""
# 1. Standard library
import json
import sys
from pathlib import Path
# 2. Third party (none in this path)
# 3. Your own modules
from . import geometry
CONSTANT = 1
def work():
...PEP 8 asks for imports at the top of the file, one per line, in three groups separated by a blank line. Alphabetical order within a group is a common convention.
When a local import is justified
def generate_report():
import csv # only needed by this one function
...
def optional_feature():
try:
import tomllib
except ImportError:
return None # the feature is simply unavailableMove an import inside a function when it is expensive and rarely needed, when it is optional, or to break a circular import - and not otherwise.
Circular imports
# File: a.py
import b
def hello():
return "a: " + b.world()
# File: b.py
import a
def world():
return "b"Running a.py starts executing it, which imports b, which imports a - already in progress and only half defined. The result is usually an ImportError or an AttributeError that looks impossible.
The fixes, in order of preference:
- Move the shared code into a third module both can import.
- Import inside the function rather than at module level.
- Import the module rather than a name from it, so the lookup happens at call time.
# File: a.py
def hello():
import b # resolved when hello() runs, not at import time
return "a: " + b.world()Common mistakes
- Naming a file after a standard library module.
- Using
from module import *. - Putting code with side effects at module level, so importing it does something.
- Expecting
importto re-run a module that is already loaded. - Creating a circular import between two modules that each need the other.
- Leaving a stale
__pycache__after renaming a file and being confused by the result.
Best practices
- Use
import moduleand callmodule.function()so origins stay visible. - Keep imports at the top, grouped and one per line.
- Give every module a docstring and define
__all__if it has a public surface. - Prefix internal names with a single underscore.
- Put anything that should run when the file is executed directly behind a main guard, covered next.
Practice
- Write a
temperature.pymodule with two conversion functions and import it three different ways. - Create a file named
math.pyin your project, importmath, and explain the failure. - Print
sys.pathand explain why the first entry matters most. - Build a deliberate circular import between two modules, then fix it three different ways.
- Add
__all__to a module and demonstrate what changes for a star import.
Conclusion
A module is a file, importing it runs it once and caches it, and Python finds it by searching sys.path with your own directory first. Prefer import module over from module import *, and never name a file after something you plan to import.