Python • Modules & Packages

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.

Topics
1
2
3
4
5
6
7

📌 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.

01

Modules

import.py files
02

Import Styles

fromas*
03

Packages

__init__.pysubpackages
04

Relative Imports

...
05

Namespace Pkgs

PEP 420no __init__
06

Built-in & pip

osrequestsvenv
07

Production

src layoutpyproject.toml

Why Modules & Packages Matter

What problem do they solve?

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.

Why are they required?

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.

Role in Production

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 (the module)
# 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
main.py (using the module)
import my_math

print(my_math.circle_area(5))       # 78.53975
print(my_math.PI)                   # 3.14159
🔍 Key Insight: When you 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.

StyleExamplePros / Cons
import moduleimport math✅ Clear namespace ❌ More typing
import module as aliasimport numpy as np✅ Short & readable
from module import namefrom math import pi✅ Direct access ❌ Possible name clash
from module import *from math import *❌ Pollutes namespace — avoid!
import_styles.py
# 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!
⚠️ Warning: 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 # Makes this directory a package ├── core.py # Module: my_package.core ├── utils.py # Module: my_package.utils └── sub_package/ ├── __init__.py └── helper.py # Module: my_package.sub_package.helper
__init__.py — controlling the public API
# my_package/__init__.py
from .core import CoreEngine
from .utils import format_data

__all__ = ["CoreEngine", "format_data"]  # controls from package import *
using the package
# 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
💡 Best Practice: Keep __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.

relative_imports_demo.py
# 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)
NotationMeaning
.Current package (sibling modules)
..Parent package
... Grandparent package (rare)
⚠️ Common Pitfall: Relative imports only work inside a package. Running a file directly (e.g., 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).

# Directory 1: /path/to/plugins/auth/ my_company/ └── plugins/ └── auth.py # importable as my_company.plugins.auth # Directory 2: /another/path/payments/ my_company/ └── plugins/ └── payments.py # same namespace: my_company.plugins.payments
namespace_usage.py
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!
🔍 When to use: Namespace packages are ideal for plugin systems, large organizations with many teams, or when you want to distribute a package in multiple parts. For most projects, regular packages with __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.

built_in_demo.py
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!
📦 Virtual Environment Setup (recommended for every project):
terminal
# 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
💡 Pro Tip: Always use a virtual environment per project. This isolates dependencies and prevents version conflicts. Modern tools like 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.

my_project/ ├── pyproject.toml # Package metadata & build config ├── README.md ├── LICENSE ├── src/ │ └── my_package/ # Actual source code │ ├── __init__.py │ ├── core.py │ └── cli.py # Entry point for CLI ├── tests/ │ ├── __init__.py │ ├── test_core.py │ └── test_cli.py └── docs/
pyproject.toml
[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"
🔍 Why src layout? It prevents accidentally importing the package from the project root during development (which would bypass the installed version). It forces you to install the package properly before testing, catching packaging bugs early.

Solved Practice Problems

🔰 Easy (3 problems)

1. Create and Import a Module

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!
2. Use a Built-in Module

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')}")
3. Import with Alias

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)

4. Package with __init__.py

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, Square
5. Relative Import Inside a Package

In 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 = radius
6. Virtual Environment & requirements.txt

Create 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)

7. __name__ == "__main__" Guard

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)) # 7
8. Dynamic Import with importlib

Import 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"}
9. Namespace Package Across Directories

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.