Lab 13: Packaging & Modules

Objective

Understand Python's module system, packaging with pyproject.toml, namespace packages, import mechanics, __init__.py design, and building distributable packages.

Time

30 minutes

Prerequisites

  • Lab 07 (Type Hints)

Tools

  • Docker image: zchencow/innozverse-python:latest


Lab Instructions

Step 1: Module System & Import Mechanics

docker run --rm zchencow/innozverse-python:latest python3 -c "
import sys
import importlib
import importlib.util
import types
import tempfile, os

# How imports work under the hood
print('=== Import Mechanics ===')
print(f'Python version: {sys.version.split()[0]}')
print(f'sys.path entries: {len(sys.path)}')
print(f'First 3 paths: {sys.path[:3]}')

# Check if module is cached
print()
import json
print(f'json is cached: {\"json\" in sys.modules}')
print(f'json.__file__: {json.__file__}')
print(f'json.__package__: {json.__package__}')

# Create a module dynamically
mod = types.ModuleType('mymodule')
mod.__doc__ = 'Dynamically created module'
mod.VERSION = '1.0.0'
mod.greet = lambda name: f'Hello, {name}!'
sys.modules['mymodule'] = mod

import mymodule
print(f'Dynamic module: {mymodule.greet(\"Dr. Chen\")}')
print(f'Dynamic module version: {mymodule.VERSION}')

# importlib — load from file path
with tempfile.TemporaryDirectory() as tmp:
    mod_file = os.path.join(tmp, 'calculator.py')
    with open(mod_file, 'w') as f:
        f.write('''
VERSION = \"2.0\"
def add(a, b): return a + b
def multiply(a, b): return a * b
class Calculator:
    def __init__(self): self.history = []
    def calc(self, op, a, b):
        result = op(a, b)
        self.history.append((op.__name__, a, b, result))
        return result
''')
    spec = importlib.util.spec_from_file_location('calculator', mod_file)
    calc_mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(calc_mod)
    print(f'Loaded calculator v{calc_mod.VERSION}')
    print(f'add(3,4) = {calc_mod.add(3, 4)}')
    c = calc_mod.Calculator()
    print(f'calc result = {c.calc(calc_mod.multiply, 6, 7)}')

# Module attributes
print()
print('=== Module Introspection ===')
import os.path
print(f'os.path is: {type(os.path).__name__}')
print(f'os.path.__name__: {os.path.__name__}')
attrs = [a for a in dir(os.path) if not a.startswith(\"_\")]
print(f'Public attrs ({len(attrs)}): {attrs[:5]}...')
"

💡 sys.modules is Python's import cache — a dictionary mapping module names to their objects. When you import json, Python first checks sys.modules; if found, it returns the cached object without re-executing the file. This is why mutating an imported module affects all importers.

📸 Verified Output:


Step 2: Package Structure & __init__.py

📸 Verified Output:


Steps 3–8: pyproject.toml, entry points, relative imports, lazy imports, __all__, Capstone

📸 Verified Output:


Summary

Concept
Key points

Module

Single .py file; cached in sys.modules

Package

Directory with __init__.py

Namespace package

Directory without __init__.py; splits across dirs

__all__

Controls from module import * exports

pyproject.toml

Modern packaging (replaces setup.py)

Lazy import

importlib.import_module() on first access

importlib.util

Load modules from arbitrary file paths

Further Reading

Last updated