Modules & Imports

Difficulty: Intermediate

Modules are the building blocks of Python's code organization system. A module is simply a .py file containing Python definitions (functions, classes, variables) and statements. Modules allow you to break large programs into smaller, manageable, and reusable pieces. Python's extensive standard library is organized as a collection of modules, and the ability to create and import your own modules is fundamental to writing maintainable software.

The import statement is the primary way to use code from other modules. When you write import math, Python searches for a module named math, executes it, and creates a namespace object. You then access its contents using dot notation: math.sqrt(16). The import system searches in a specific order: the current directory, then the directories listed in the PYTHONPATH environment variable, and finally the standard library paths. This search order is stored in sys.path and can be inspected or modified at runtime.

The from...import syntax lets you import specific names directly into your current namespace. With from math import sqrt, pi you can use sqrt() and pi directly without the math. prefix. While this is more concise, it can lead to naming conflicts if two modules export the same name. The from module import * syntax imports everything, which is generally discouraged in production code because it pollutes the namespace and makes it unclear where names come from. A good rule: use import module for standard library modules and from module import specific_thing for your own modules.

The as keyword creates an alias for an imported module or name. This is useful for long module names (import numpy as np) or to avoid name collisions (from datetime import datetime as dt). Aliasing is purely a convenience feature and does not change what is imported. Some aliases are so widespread they are effectively standard: np for numpy, pd for pandas, plt for matplotlib.pyplot.

The if __name__ == '__main__' idiom is a critical pattern in Python. Every module has a special __name__ attribute. When a module is run directly (python mymodule.py), __name__ is set to '__main__'. When it is imported by another module, __name__ is set to the module's actual name (e.g., 'mymodule'). This lets you write code that behaves differently when run as a script versus when imported as a library. Test code, example usage, and CLI entry points are typically placed inside this guard.

Code examples

Import Styles

# Style 1: import the whole module
import math
print(math.sqrt(144))
print(math.pi)

# Style 2: import specific names
from math import factorial, gcd
print(factorial(5))
print(gcd(24, 36))

# Style 3: import with alias
import math as m
print(m.ceil(4.3))
print(m.floor(4.7))

Style 1 keeps the namespace clean by requiring the module prefix. Style 2 is concise for frequently used names. Style 3 shortens long module names. All three are valid; choose based on readability and convention.

The __name__ Variable

# When this file is run directly: __name__ == '__main__'
# When imported by another file: __name__ == 'module_name'

def greet(name):
    return f"Hello, {name}!"

def main():
    print(greet("World"))
    print(f"Module name: {__name__}")

# This block only runs when the file is executed directly
if __name__ == "__main__":
    main()

__name__ is '__main__' when the script is run directly. This pattern lets a file serve as both a reusable library (when imported) and a standalone script (when run directly). Functions defined outside the if block are available for import either way.

Creating and Using a Module

# Imagine we have a module called 'mathutils.py' containing:
# def add(a, b): return a + b
# def multiply(a, b): return a * b
# PI = 3.14159

# We can simulate it here:
def add(a, b):
    return a + b

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

PI = 3.14159

# Using the 'module'
result1 = add(10, 20)
result2 = multiply(5, 6)
print(f"add(10, 20) = {result1}")
print(f"multiply(5, 6) = {result2}")
print(f"PI = {PI}")

A module is just a .py file with functions, classes, and variables. Other files import it using its filename (without .py). Organizing related functionality into modules makes code reusable and easier to maintain.

Inspecting Module Contents

import math

# dir() lists all names in a module
math_funcs = [name for name in dir(math) if not name.startswith('_')]
print(f"Number of public names in math: {len(math_funcs)}")
print(f"First 5: {math_funcs[:5]}")

# hasattr() checks if a module has a specific attribute
print(f"Has sqrt: {hasattr(math, 'sqrt')}")
print(f"Has power: {hasattr(math, 'power')}")

dir() returns all names defined in a module. We filter out private names starting with underscore. hasattr() is useful for checking if a module provides a specific function before calling it.

Key points

Concepts covered

import Statement, from...import, as Alias, __name__ == '__main__', Creating Modules