Module System — import Styles, __name__, __all__
In Python, a module is a single Python file with a .py extension. It is the core unit for separating variables, functions, and classes into files for reuse and systematic code organization.
What Is a Module
Every .py file is a module. Creating math.py makes it a math module in its own right.
# mymodule.py
PI = 3.14159
def greet(name: str) -> str:
return f"Hello, {name}!"
class Calculator:
def add(self, a: int, b: int) -> int:
return a + b
4 Ways to Import
1. import module_name
import math
print(math.sqrt(16)) # 4.0
print(math.pi) # 3.141592653589793
The namespace is clearly maintained. This is the most recommended style.
2. from module import name
from math import sqrt, pi
print(sqrt(25)) # 5.0
print(pi) # 3.141592653589793
Useful when importing only specific items you use frequently.
3. import module as alias
import numpy as np
import pandas as pd
arr = np.array([1, 2, 3])
Used to shorten long module names or avoid naming conflicts.
4. from module import * (not recommended)
from math import * # Imports all public names into current namespace
print(floor(3.7)) # 3
Avoid in production code as it pollutes the namespace.
__name__ == "__main__" Pattern
When a Python file is run directly, the __name__ variable becomes "__main__". When imported from another file, it becomes the module name.
# calculator.py
def add(a: int, b: int) -> int:
return a + b
def subtract(a: int, b: int) -> int:
return a - b
if __name__ == "__main__":
# Code that only runs when executed directly
print("Calculator test:")
print(f"5 + 3 = {add(5, 3)}")
print(f"10 - 4 = {subtract(10, 4)}")
# Direct execution: __name__ == "__main__" → test code runs
python calculator.py
# Import: __name__ == "calculator" → test code does not run
python -c "import calculator; print(calculator.add(1, 2))"
This pattern is essential for module testing, separating CLI entry points, and dual-purpose scripts/libraries.
__all__ — Defining Public API
__all__ defines the list of names to expose when using from module import *. It also documents the module's intentional public API.
# utils.py
__all__ = ["public_func", "PublicClass"]
def public_func():
"""Public function"""
return "public"
def _helper():
"""Internal helper — not intended for external use"""
return "internal"
class PublicClass:
pass
class _InternalClass:
pass
from utils import *
public_func() # Works fine
# _helper() # NameError: _helper was not imported
If __all__ is not defined, all names that don't start with an underscore (_) are subject to * import.
Module Search Path: sys.path
Python searches for modules in the following order when importing:
- Directory of the currently running script
- Paths specified in the
PYTHONPATHenvironment variable - Installed standard library paths
- site-packages (packages installed with pip)
import sys
# Check module search path
for path in sys.path:
print(path)
# Add search path at runtime
sys.path.insert(0, "/my/custom/lib")
import my_custom_module # Now findable
# Add path via environment variable (permanent)
# export PYTHONPATH="/my/custom/lib:$PYTHONPATH"
Dynamic Import with importlib
Import modules at runtime by specifying the module name as a string. Used in plugin systems and configuration-based loaders.
import importlib
# Dynamically import module by string
module_name = "math"
math = importlib.import_module(module_name)
print(math.sqrt(9)) # 3.0
# Import submodule within a package
json = importlib.import_module("json")
data = json.loads('{"key": "value"}')
# Plugin system example
from importlib import import_module
def load_plugin(plugin_name: str):
"""Dynamically load a plugin"""
try:
module = import_module(f"plugins.{plugin_name}")
return module.Plugin()
except ModuleNotFoundError:
raise ValueError(f"Plugin '{plugin_name}' not found.")
# Plugin name determined at runtime
plugin = load_plugin("csv_exporter")
plugin.export(data)
# Load directly from a file path with importlib.util
import importlib.util
spec = importlib.util.spec_from_file_location(
"my_module", "/absolute/path/to/my_module.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
module.my_function()
Circular Import Problem and Solutions
A circular import occurs when two modules import each other.
# a.py — problem
from b import func_b # imports b
def func_a():
return "A"
# b.py — problem
from a import func_a # imports a → circular!
def func_b():
return "B"
Solution 1: Move import inside a function
# a.py
def func_a():
return "A"
def combined():
from b import func_b # Import at the time of function call
return func_a() + func_b()
Solution 2: Extract to a shared module
# common.py — shared dependency
def shared_utility():
return "shared"
# a.py
from common import shared_utility
# b.py
from common import shared_utility
Solution 3: Use TYPE_CHECKING for type hints
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from b import ClassB # Only imported during type checking
def func_a(obj: "ClassB") -> None: # String annotation
pass
Practical Example: Config Loader Module
# config_loader.py
"""Module for loading config files in various formats"""
from __future__ import annotations
import importlib
import json
import os
from pathlib import Path
from typing import Any
__all__ = ["load_config", "ConfigLoader"]
def load_config(path: str | Path, format: str = "auto") -> dict[str, Any]:
"""Loads a configuration file."""
path = Path(path)
if format == "auto":
format = path.suffix.lstrip(".")
loader_map = {
"json": _load_json,
"toml": _load_toml,
"env": _load_env,
}
loader = loader_map.get(format)
if loader is None:
raise ValueError(f"Unsupported format: {format}")
return loader(path)
def _load_json(path: Path) -> dict[str, Any]:
with open(path, encoding="utf-8") as f:
return json.load(f)
def _load_toml(path: Path) -> dict[str, Any]:
try:
import tomllib # Python 3.11+
except ImportError:
tomllib = importlib.import_module("tomli") # Third-party fallback
with open(path, "rb") as f:
return tomllib.load(f)
def _load_env(path: Path) -> dict[str, str]:
result = {}
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#"):
key, _, value = line.partition("=")
result[key.strip()] = value.strip()
return result
class ConfigLoader:
"""Config loader class"""
def __init__(self, base_dir: str | Path = "."):
self.base_dir = Path(base_dir)
self._cache: dict[str, dict] = {}
def get(self, filename: str) -> dict[str, Any]:
if filename not in self._cache:
self._cache[filename] = load_config(self.base_dir / filename)
return self._cache[filename]
def reload(self, filename: str) -> dict[str, Any]:
self._cache.pop(filename, None)
return self.get(filename)
if __name__ == "__main__":
# Test when run directly
loader = ConfigLoader(".")
print("ConfigLoader test complete")
Expert Tips
Tip 1: Access package resources with importlib.resources
from importlib.resources import files
# Read a data file inside a package
data = files("mypackage").joinpath("data/config.json").read_text()
Tip 2: Manipulate the import cache with sys.modules
import sys
# List of already imported modules
print("math" in sys.modules) # True (if ever imported)
# Force reload a module
import importlib
importlib.reload(sys.modules["mymodule"])
Tip 3: Use importlib.import_module instead of __import__
# Old-style (not recommended)
mod = __import__("math")
# Modern approach
import importlib
mod = importlib.import_module("math")
Tip 4: Conditional import pattern
try:
import ujson as json # Try fast JSON library
except ImportError:
import json # Fall back to standard library
# Use json variable uniformly from here
data = json.loads('{"key": "value"}')