Skip to content

Python Modules

A module is the basic unit of code reuse in Python — every .py file is a module. This article covers the import mechanism, how to organize packages, and best practices.

Three Sources of Modules

  • Built-in modules: installed with the interpreter, e.g. os, sys, json, re.
  • Third-party modules: installed via pip install, e.g. requests, pydantic.
  • Custom modules: .py files you write yourself within your project.

The import Statement

# import the entire module (recommended: keeps the namespace clear)
import os
import json

print(os.getcwd())
data = json.dumps({"key": "value"})

# import specific names (reduces prefix usage, suitable for frequently used names)
from pathlib import Path
from datetime import datetime

p = Path(".")
now = datetime.now()

# use aliases (when names are too long or to avoid conflicts)
import numpy as np
from collections import defaultdict as dd

# import multiple modules (avoid writing them on one line — hurts readability)
import os
import sys
import re
from module import * imports all public names from the module into the current namespace and can easily cause name collisions. It is not recommended. Module authors can define an __all__ list in their file to control what * exports.

What Happens During Import

# foo.py
x = 1
print("foo module executed")

def get():
    return x

The first time you run import foo, three things happen:

  1. All top-level code in foo.py is executed (only once).
  2. A new namespace is created to hold the names produced during execution.
  3. The name foo is created in the current namespace, pointing to the new namespace.
import foo           # prints "foo module executed"
import foo           # no re-execution (already cached in sys.modules)

print(foo.x)         # 1
print(foo.get())     # 1

sys.modules — Module Cache

import sys

# check which modules are already loaded
print("json" in sys.modules)   # True (if json was imported earlier)

# force a reload (useful during development after modifying a module)
import importlib
import foo
importlib.reload(foo)

Module Search Path

When importing, Python searches for modules in this order:

  1. sys.modules (in-memory cache)
  2. Built-in modules (sys.builtin_module_names)
  3. Directories in sys.path (left to right)
import sys
print(sys.path)
# ['', '/usr/lib/python312.zip', '/usr/lib/python3.12', ...]
# The first element '' represents the directory of the running script

# temporarily add a search path
sys.path.insert(0, "/my/custom/modules")

Packages

A package is a directory containing an __init__.py file, used to organize multiple modules:

mypackage/
├── __init__.py        # package entry point, can be empty
├── core.py
├── utils.py
└── sub/
    ├── __init__.py
    └── helper.py
# import modules from a package
import mypackage.core
from mypackage import utils
from mypackage.sub import helper
from mypackage.sub.helper import some_func

# __init__.py can define the package's public interface
# mypackage/__init__.py
# from .core import CoreClass
# from .utils import helper_func
# __all__ = ["CoreClass", "helper_func"]

Relative Imports (used inside a package)

# inside mypackage/core.py
from .utils import format_data        # same package
from ..other_pkg import something     # parent package

Relative imports are only valid inside a package and cannot be used in top-level scripts.

The if __name__ == "__main__" Guard

Every .py file has the built-in variable __name__:

  • When run as the main script: __name__ == "__main__"
  • When imported as a module: __name__ == "module_name"
# mymodule.py

def add(a: int, b: int) -> int:
    return a + b

# only runs when executed directly; skipped when imported
if __name__ == "__main__":
    print(add(1, 2))   # 3
    print(add(10, 20)) # 30

Circular Import Issues

# a.py
from b import func_b   # ← b.py hasn't finished loading a.py yet

# b.py
from a import func_a   # ← a.py hasn't finished loading b.py yet

Circular imports usually indicate a design problem. Solutions:

  1. Extract shared data/functions into a third module.
  2. Move the import statement inside a function (lazy import).
# b.py — lazy import
def func_b():
    from a import func_a  # imported at call time, avoids circular dependency at initialization
    return func_a()

Standard Convention: Import Order

According to PEP 8, import statements should appear at the top of the file, grouped into three sections separated by blank lines:

# 1. Standard library modules
import os
import sys
from pathlib import Path

# 2. Third-party modules
import requests
from pydantic import BaseModel

# 3. Local / project modules
from myapp.core import Engine
from myapp.utils import logger

Tools like ruff or isort can automatically sort imports for you.

Last updated on