Modules & Packages Complete Guide
Master Python's modular architecture — from basic imports to production-grade package structures with __init__.py, namespace packages, pip, virtual environments, and real-world patterns.
📌 What you'll learn: Modules let you split code into reusable files. Packages group related modules into directories. Together they form the backbone of Python's scalability — from a single script to enterprise applications with hundreds of files.
🎯 Goal: Organize code like a pro using modules, packages, proper import styles, __init__.py, relative imports, and virtual environments — whether you're building a CLI tool or a web application.
Modules
Import Styles
Packages
Relative Imports
Namespace Pkgs
Built-in & pip
Production
Why Modules & Packages Matter
A single .py file with 5000 lines is a nightmare to debug. Modules break code into logical, reusable units. Packages organize related modules, preventing name clashes and making large codebases manageable.
Without modules, every project would be one giant file. Without packages, all module names would have to be globally unique. Python's import system solves both: namespacing, encapsulation, and discoverability.
Production projects use a src/ layout, pyproject.toml for metadata, virtual environments for isolation, and tools like pip, uv, or poetry for dependency management. Well-structured packages are publishable on PyPI.
1 Modules – Every .py File Is a Module
A module is simply a Python file (extension .py) containing functions, classes, or variables. You can import it and use its contents in another file. Python ships with dozens of built-in modules (like math, os, json).
# my_math.py — a custom module
PI = 3.14159
def circle_area(radius):
"""Return area of a circle."""
return PI * radius ** 2
def circle_circumference(radius):
return 2 * PI * radius
import my_math
print(my_math.circle_area(5)) # 78.53975
print(my_math.PI) # 3.14159
import my_math, Python searches sys.path (current directory first, then standard library, then site-packages). The module's code runs once and a module object is cached in sys.modules.2 Import Styles – Choose the Right One
Python offers several import syntaxes. Each has trade-offs between readability, namespace pollution, and convenience.
| Style | Example | Pros / Cons |
|---|---|---|
import module | import math | ✅ Clear namespace ❌ More typing |
import module as alias | import numpy as np | ✅ Short & readable |
from module import name | from math import pi | ✅ Direct access ❌ Possible name clash |
from module import * | from math import * | ❌ Pollutes namespace — avoid! |
# Recommended styles
import math # Use math.sqrt(16)
import numpy as np # Use np.array([1,2])
from datetime import datetime # Use datetime.now()
# Avoid this in production:
from math import * # Don't do this!
from module import * imports all public names (those not starting with _) into the current namespace. This can silently overwrite existing names and makes code hard to debug.3 Packages – Directories with __init__.py
A package is a directory containing an __init__.py file (and optionally subpackages). The __init__.py can be empty or can initialize the package, expose a public API, or run setup code.
# my_package/__init__.py
from .core import CoreEngine
from .utils import format_data
__all__ = ["CoreEngine", "format_data"] # controls from package import *
# Option A: import the package
import my_package
engine = my_package.CoreEngine()
# Option B: import specific names
from my_package import CoreEngine, format_data
# Option C: import subpackage module
from my_package.sub_package import helper
__init__.py minimal. Import only the most important classes/functions. This makes the package's public API clear and avoids circular import problems.4 Relative Imports – Within a Package
Inside a package, use relative imports (starting with .) to refer to sibling modules or parent packages. This makes the package self-contained and movable.
# Inside my_package/core.py
from .utils import format_data # sibling module (single dot)
from .sub_package.helper import run # subpackage module
from .. import some_sibling_package # parent package (double dot)
def process():
data = format_data("raw")
run(data)
| Notation | Meaning |
|---|---|
. | Current package (sibling modules) |
.. | Parent package |
... | Grandparent package (rare) |
python core.py) that uses relative imports will raise ImportError: attempted relative import with no known parent package. Always run from the project root or use python -m my_package.core.5 Namespace Packages – PEP 420
Python 3.3+ supports namespace packages — packages without __init__.py that can span multiple directories. This allows splitting a logical package across separate physical locations (useful for plugins and large monorepos).
import sys
sys.path.extend(["/path/to/plugins/auth", "/another/path/payments"])
from my_company.plugins import auth, payments
# Both work even though they live in different directories!
__init__.py are simpler and recommended.6 Built-in Modules, pip & Virtual Environments
Python's standard library is vast: os, sys, json, re, datetime, collections, itertools, pathlib, sqlite3, and many more. For everything else, use pip to install third-party packages from PyPI.
import os
import json
from pathlib import Path
from collections import defaultdict
# Work with paths
home = Path.home()
config_file = home / ".myapp" / "config.json"
# JSON serialization
data = {"users": ["alice", "bob"], "active": True}
print(json.dumps(data, indent=2))
# defaultdict avoids KeyError
counts = defaultdict(int)
counts["apples"] += 1 # no KeyError!
# Create a virtual environment
python -m venv .venv
# Activate it (Linux/macOS)
source .venv/bin/activate
# Activate it (Windows)
.venv\Scripts\activate
# Install packages
pip install requests pandas
# Freeze dependencies
pip freeze > requirements.txt
uv (by Astral) are 10-100x faster than pip.7 Production-Grade Package Structure
A well-organized project uses the src layout, a pyproject.toml for metadata, and separates source code from tests and configuration.
[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my_package"
version = "0.1.0"
description = "A production-ready Python package"
requires-python = ">=3.10"
dependencies = ["requests>=2.28", "click>=8.0"]
[project.scripts]
mycli = "my_package.cli:main"
Solved Practice Problems
🔰 Easy (3 problems)
Create a file greetings.py with a function hello(name). Import and use it in main.py.
# greetings.py
def hello(name="World"):
return f"Hello, {name}!"
# main.py
import greetings
print(greetings.hello("Alice")) # Hello, Alice!Use the datetime module to print today's date and the current time.
from datetime import datetime, date
today = date.today()
now = datetime.now()
print(f"Date: {today}, Time: {now.strftime('%H:%M:%S')}")Import numpy as np and create a simple array.
import numpy as np
arr = np.array([1, 2, 3, 4, 5])
print(arr * 2) # [2 4 6 8 10]⚡ Intermediate (3 problems)
Create a package shapes with modules circle.py and square.py. Expose both via __init__.py.
# shapes/__init__.py
from .circle import Circle
from .square import Square
__all__ = ["Circle", "Square"]
# Usage
from shapes import Circle, SquareIn shapes/circle.py, import a utility function from shapes/utils.py using a relative import.
# shapes/circle.py
from .utils import validate_radius
class Circle:
def __init__(self, radius):
validate_radius(radius)
self.radius = radiusCreate a venv, install requests, and generate requirements.txt.
python -m venv .venv
source .venv/bin/activate # or .venv\Scripts\activate on Windows
pip install requests
pip freeze > requirements.txt🚀 Advanced (3 problems)
Write a module that can be both imported and run as a script.
# calculator.py
def add(a, b): return a + b
def subtract(a, b): return a - b
if __name__ == "__main__":
# Runs only when executed directly
print("Testing calculator:")
print(add(3, 4)) # 7
print(subtract(10, 3)) # 7Import a module by name (string) at runtime.
import importlib
module_name = "json"
json_module = importlib.import_module(module_name)
data = json_module.dumps({"key": "value"})
print(data) # {"key": "value"}Set up two directories containing myorg/plugins/ and demonstrate that both are importable.
import sys
sys.path.extend(["/path/plugins-auth", "/path/plugins-pay"])
from myorg.plugins import auth, payments
auth.login()
payments.process()Unsolved Practice Problems
🔰 Easy (3 problems)
1. Create a Config Module
Create config.py with variables DEBUG=True, PORT=8080. Import and print them in app.py.
2. Use the random Module
Import random and write a function that returns a random integer between 1 and 100. Call it 5 times.
3. Import from a Subdirectory
Create a directory lib/ with an __init__.py and a module helpers.py. Import a function from lib.helpers in your main script.
⚡ Intermediate (3 problems)
4. Circular Import Fix
Module A imports from B, and B imports from A. Refactor to break the circular dependency using a third module C.
5. Package with __all__
Create a package animals with cat.py, dog.py, _internal.py. Set __all__ in __init__.py to expose only Cat and Dog.
6. Install from requirements.txt
Given a requirements.txt with flask and sqlalchemy, create a venv and install all dependencies.
🚀 Advanced (3 problems)
7. Lazy Import Pattern
Write a module that defers importing a heavy library (like pandas) until a function is actually called.
8. Plugin Discovery System
Write code that scans a plugins/ directory and imports all .py files as modules dynamically.
9. Build a CLI Entry Point
Create a package with pyproject.toml that defines a [project.scripts] entry point. Install it with pip install -e . and test the CLI command.