Chapter Twelve

Modules and Packages

Learning Objectives
  1. Explain what a module is and use import, from...import, and import...as to load code from other files
  2. Describe how Python searches for modules using sys.path and predict which module will be found
  3. Use the if __name__ == '__main__' idiom to write files that work both as scripts and importable modules
  4. Organise code into packages using directories with __init__.py and choose between absolute and relative imports
  5. Diagnose and resolve circular imports by restructuring code or deferring imports

A single file can only take you so far. The moment your program grows beyond a few hundred lines, you need a way to split it into manageable pieces — pieces that can be developed, tested, and reasoned about independently. Python's answer is beautifully simple: any .py file is a module, and any directory of modules is a package. There is no special compilation step, no configuration file, no build system. You write a file, you import it, and it works.

What Is a Module

A module is any Python file. If you create a file called helpers.py containing a function, that file is a module named helpers:

# helpers.py
def greet(name):
    return f"Hello, {name}!"

PI = 3.14159

From another file in the same directory, you can import it:

import helpers

print(helpers.greet("Alice"))  # Output: Hello, Alice!
print(helpers.PI)              # Output: 3.14159

When Python imports a module, it executes the file from top to bottom — every statement runs. Functions and classes are defined, variables are assigned, and any top-level code executes. The resulting namespace becomes the module object, and you access its contents with dot notation.

Importantly, Python only executes a module once per interpreter session. If two files both import helpers, the file runs the first time and is cached after that. Subsequent imports reuse the cached module object. This is why putting print("Loading!") at the top level of a module only prints once, no matter how many files import it.

Import Variations

Python offers several ways to import. The standard import brings in the entire module:

import math
print(math.sqrt(16))    # Output: 4.0

from...import pulls specific names into your namespace:

from math import sqrt, pi
print(sqrt(16))          # Output: 4.0
print(pi)                # Output: 3.141592653589793

import...as gives the module (or name) an alias:

import numpy as np       # standard convention
from datetime import datetime as dt

Each form has its place. import math is safest — every use of math.sqrt makes it clear where sqrt comes from. from math import sqrt is convenient when you use a name frequently and the origin is obvious. The wildcard from math import * dumps everything from the module into your namespace and should be avoided in production code — it makes it impossible to know where names come from and can silently overwrite existing names.

The Module Search Path

When you write import helpers, Python needs to find helpers.py. It searches a list of directories stored in sys.path:

import sys
for p in sys.path:
    print(p)

The search order is:

  1. The directory containing the script being run (or the current directory in an interactive session).
  2. Directories listed in the PYTHONPATH environment variable, if set.
  3. The standard library directories.
  4. The site-packages directory (where pip installs third-party packages).

Python uses the first match it finds. This means a file named random.py in your project directory will shadow the standard library's random module — a common and bewildering source of bugs. The error message gives no hint that shadowing is the problem; you simply get an AttributeError when you try to use a function that your file does not define. Never name your files after standard library modules. Common offenders include random.py, email.py, test.py, collections.py, and string.py.

The name Guard

Every module has a special attribute called __name__. When a file is run directly as a script, __name__ is set to "__main__". When it is imported as a module, __name__ is set to the module's name:

# calculator.py
def add(a, b):
    return a + b

def multiply(a, b):
    return a * b

if __name__ == "__main__":
    # This block runs only when the file is executed directly
    print(add(3, 4))       # Output: 7
    print(multiply(3, 4))  # Output: 12

The if __name__ == "__main__" idiom is one of Python's most important patterns. It lets you write files that work both as importable modules and as standalone scripts. Without it, any test code or demonstration logic at the top level would run every time someone imports your module — which is almost never what you want.

Packages

A package is a directory containing Python modules and a special file called __init__.py:

myproject/
├── __init__.py
├── models.py
├── views.py
└── utils/
    ├── __init__.py
    ├── formatting.py
    └── validation.py

The __init__.py file can be empty — its presence alone tells Python that the directory is a package. You import from packages using dot notation:

from myproject.models import User
from myproject.utils.formatting import format_date
import myproject.views

The __init__.py file runs when the package is first imported. You can use it to define the package's public API — importing key names so users can access them conveniently:

# myproject/__init__.py
from .models import User
from .views import render_page

Now users can write from myproject import User instead of from myproject.models import User. This is a common pattern in well-designed libraries: the internal module structure is an implementation detail, and the __init__.py exposes a clean, flat API. Users import from the package; they do not need to know which sub-module contains what.

Absolute vs Relative Imports

Within a package, you can import sibling modules using absolute imports (the full path from the project root) or relative imports (using dots to navigate from the current module):

# Inside myproject/views.py

# Absolute import
from myproject.models import User

# Relative import (single dot = current package)
from .models import User

# Relative import (double dot = parent package)
from ..utils.formatting import format_date

Absolute imports are clearer and work regardless of where the module sits in the hierarchy. They are recommended by PEP 8. Relative imports are shorter and survive package renaming — if you rename myproject to webapp, relative imports still work. In practice, most teams pick one style and stick with it.

Relative imports only work inside packages. If you run a file directly with python myproject/views.py, relative imports will fail with an ImportError because Python does not know the file is part of a package. Run the file as a module instead: python -m myproject.views. The -m flag tells Python to locate the module within the package hierarchy, which enables relative imports and sets __name__ and __package__ correctly.

Namespace Packages

Since Python 3.3, a directory without __init__.py can still be a package — a namespace package. This is an advanced feature designed for splitting a single logical package across multiple directories on disk:

/path/a/mypkg/foo.py
/path/b/mypkg/bar.py

If both /path/a and /path/b are on sys.path, you can import both mypkg.foo and mypkg.bar even though mypkg exists in two separate locations with no __init__.py. This is primarily used by large frameworks and plugin systems. For everyday projects, always include __init__.py in your packages — it is explicit, portable, and avoids confusing edge cases.

Circular Imports

A circular import occurs when module A imports module B, and module B imports module A. Python handles this better than you might expect — it does not immediately crash — but it can produce confusing ImportError or AttributeError when a module tries to use something from the other module that has not been defined yet:

# a.py
from b import helper_b
def helper_a():
    return "from A"

# b.py
from a import helper_a    # ImportError — helper_a not defined yet
def helper_b():
    return helper_a()

The fix is usually structural. Move the shared code into a third module that both can import. Alternatively, defer the import to inside the function that needs it:

# b.py
def helper_b():
    from a import helper_a   # import happens at call time, not module load time
    return helper_a()

Deferred imports work but they are a code smell — they suggest your modules are too tightly coupled. If you find yourself needing circular imports, it is time to rethink your module boundaries. The usual solution is to extract the shared code into a third module that both can import, breaking the cycle. Good module design, like good function design, is about clear boundaries and single responsibilities.

Organising a Project

As a project grows, you need a consistent layout. The widely recommended src layout separates your source code from tests, configuration, and other files:

my-project/
├── src/
│   └── mypackage/
│       ├── __init__.py
│       ├── core.py
│       └── utils.py
├── tests/
│   ├── test_core.py
│   └── test_utils.py
├── pyproject.toml
└── README.md

The src/ directory prevents accidental imports of your package from the project root — you must install the package (even in editable mode with pip install -e .) before importing it. This catches import errors early and ensures your tests run against the same code that your users will install. The pyproject.toml file defines your project's metadata and dependencies — we will cover it in detail in the chapter on environments and packaging.

Python's module system is one of those things that seems trivially simple — and it is, until you need it not to be. Import a file, use its contents, move on. But that simplicity rests on a carefully designed set of rules about search paths, namespaces, and execution order. Understanding those rules means understanding why your imports work, why they sometimes do not, and how to organise a project so that they always will.