Python Modules and Packages — Organize Your Code Like a Pro
Advertisement
Introduction
Why This Matters
As Python projects grow, organizing code into modules and packages becomes essential. Without structure, a project becomes a single massive file with entangled logic that is impossible to test, maintain, or share. Modules and packages are Python's mechanism for creating clean boundaries between concerns.
Understanding Python's import system also helps you avoid common bugs like circular imports, name conflicts, and import order issues that trip up even experienced developers. If you plan to publish a library on PyPI, build a Django or FastAPI application, or work on any collaborative Python project, mastering modules and packages is non-negotiable.
This guide covers everything from simple import statements to creating installable packages with pyproject.toml.
What is a Module?
A module is any .py file. When you write import json, Python loads the json module from the standard library.
# math_utils.py
PI = 3.14159
def area_of_circle(radius: float) -> float:
"""Calculate area of a circle."""
return PI * radius ** 2
def circumference(radius: float) -> float:
"""Calculate circumference of a circle."""
return 2 * PI * radius# main.py
import math_utils
print(math_utils.area_of_circle(5)) # 78.53975
print(math_utils.PI) # 3.14159
# Selective import
from math_utils import area_of_circle, PI
print(area_of_circle(3))
# Alias
import math_utils as mu
print(mu.circumference(4))The if name == "main" Guard
# utils.py
def helper():
return "I am a helper"
def main():
print("Running utils.py directly")
print(helper())
if __name__ == "__main__":
main()
# This block only runs when utils.py is executed directly,
# not when it is imported by another module.What is a Package?
A package is a directory containing an __init__.py file. It groups related modules.
myapp/
__init__.py
models/
__init__.py
user.py
product.py
services/
__init__.py
auth.py
payment.py
utils/
__init__.py
helpers.py# myapp/models/user.py
class User:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
def __repr__(self):
return f"User(name={self.name!r})"# myapp/models/__init__.py
from .user import User
from .product import Product
__all__ = ["User", "Product"]# Usage from outside the package
from myapp.models import User
from myapp.services.auth import verify_tokenImport Styles and When to Use Each
# Absolute import (preferred — explicit and unambiguous)
from myapp.models.user import User
import myapp.services.auth
# Relative import (inside a package — avoids hardcoding package name)
# In myapp/services/payment.py:
from ..models.user import User # go up one level, then into models
from .auth import verify_token # same levelinit.py — Controlling the Public API
# myapp/__init__.py
from .models import User, Product
from .services.auth import verify_token
__version__ = "1.0.0"
__all__ = ["User", "Product", "verify_token"]Now users can simply write:
from myapp import User, verify_tokenUseful Standard Library Modules
import os # OS interaction, paths, env vars
import sys # Python runtime info
import json # JSON serialization
import re # Regular expressions
import datetime # Dates and times
import pathlib # Object-oriented path handling
import collections # deque, Counter, defaultdict, namedtuple
import itertools # Iteration utilities
import functools # Decorators, partial, lru_cache
import logging # Structured logging
import argparse # CLI argument parsing
import dataclasses # dataclass decorator
import typing # Type hints
import asyncio # Async/await supportCreating an Installable Package
Modern Python packages use pyproject.toml:
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.backends.legacy:build"
[project]
name = "myapp"
version = "1.0.0"
description = "My Python package"
authors = [{name = "Alice", email = "alice@example.com"}]
requires-python = ">=3.10"
dependencies = ["pydantic>=2.0", "httpx>=0.27"]
[project.urls]
Homepage = "https://github.com/alice/myapp"Install in development mode:
pip install -e .Common Mistakes
- Naming a file the same as a standard library module (e.g.,
json.py,os.py) — shadows the built-in - Using
from module import *— pollutes the namespace and makes dependencies opaque - Circular imports — Module A imports from B, and B imports from A; restructure to avoid
- Missing
__init__.pyin Python 3 — not required for namespace packages but needed for regular packages - Importing at the function level instead of the module level when imports are expensive
Best Practices
- Use absolute imports everywhere for clarity
- Define
__all__in__init__.pyto control your public API - Avoid circular imports by moving shared types to a separate
types.pyormodels.py - Keep
__init__.pyfiles thin — just re-exports, no business logic - Use
pyproject.tomlinstead of the oldsetup.pyfor new packages
Key Takeaways
- A module is any
.pyfile; a package is a directory with__init__.py if __name__ == "__main__"guards code that should only run when the file is executed directly- Absolute imports are preferred over relative imports for clarity and refactoring safety
__init__.pydefines the package's public API by controlling what is re-exported- Circular imports are a design smell — restructure by introducing a shared module
pyproject.tomlis the modern standard for defining installable Python packages__all__controls what is exported whenfrom module import *is used
Advertisement