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.

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.14159
geometry module loaded
12.56636
3.14159

Notice 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
FormYou writeUse when
import mm.thingDefault. The origin of every name stays visible.
import m as xx.thingThe name is long or conventionally abbreviated.
from m import thingthingYou use one or two names heavily.
from m import *thingAlmost 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 entirely

A 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)      # True

Python 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 session

Where 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 packages

Python 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 what from 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 unavailable

Move 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:

  1. Move the shared code into a third module both can import.
  2. Import inside the function rather than at module level.
  3. 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 import to 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 module and call module.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

  1. Write a temperature.py module with two conversion functions and import it three different ways.
  2. Create a file named math.py in your project, import math, and explain the failure.
  3. Print sys.path and explain why the first entry matters most.
  4. Build a deliberate circular import between two modules, then fix it three different ways.
  5. 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.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →
Python

Sorting Algorithms

Python sorts for you in n log n. Implementing bubble, insertion, merge and quick sort is still worth doing, because it teaches how algorithms are comp...

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.