<< Home

Chapter 9: Python Modules and Packages

As programs grow, organisation becomes as important as syntax. Python’s answer is the module system. A module is a single .py file that defines names (functions, classes, variables) inside its own namespace. A package is a directory that groups related modules together so you can build clear, reusable libraries rather than a single monolithic script.

Importing brings names from one module into another. The import machinery locates a module (or package), compiles it to bytecode if needed, executes it once to create its globals, then caches the result in sys.modules. Later imports reuse that cached module. This behaviour is simple to use in small scripts, yet it scales to large codebases because each file has a clean boundary and a predictable namespace.

Packages let you layer structure. You can arrange modules into folders that mirror the concepts of your project, then import with dotted paths. Since Python 3.3, namespace packages also allow package directories without an __init__.py file (useful when splitting a package across multiple locations). Traditional packages still use __init__.py to initialise package-level behaviour or expose selected symbols for import.

Import mechanics and aliasing

The import statement is one of Python’s most important features. It lets you bring external code into the current namespace so that you can reuse functionality without rewriting it. When Python encounters an import, it searches through a list of directories defined in sys.path, which includes the current working directory, the standard library, and any site-packages directories for installed modules.

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

Once imported, a module is executed only once per session. After that, the interpreter retrieves it from the cache stored in sys.modules. This means that repeated imports are efficient and fast. If you need to reload a module after modifying it interactively, you can use importlib.reload().

import importlib
importlib.reload(math)

Python also supports importing with an alias using the as keyword. This is a simple yet powerful way to shorten long module names or clarify context in your code.

import numpy as np
import datetime as dt
💡 Aliasing is particularly useful in libraries with common conventions, such as import pandas as pd or import matplotlib.pyplot as plt. Following community norms helps readability for anyone familiar with those libraries.

You can also import specific names from a module to avoid repeating the module prefix each time you use a function or class:

from math import sqrt, pi
print(sqrt(9) * pi)
⚠️ Avoid using from module import * except in interactive sessions. It pollutes the namespace with unknown names and makes code harder to read and debug.

Understanding how imports resolve, execute, and cache helps prevent confusion when working with multiple files. It ensures that your codebase remains modular, efficient, and predictable.

The module search path

When Python imports a module, it follows a well-defined search process looking through a sequence of directories known as the module search path. This path is stored in the list sys.path and is initialised each time the interpreter starts.

Understanding the search path is crucial when structuring projects or debugging import errors. By knowing where Python looks (and in what order) you can predict and control how your modules are found and loaded. The typical search order is: