Skip to main content
Advertisement

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.

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:

  1. Directory of the currently running script
  2. Paths specified in the PYTHONPATH environment variable
  3. Installed standard library paths
  4. 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"}')
Advertisement