Python Modules and Packages — Organize Your Code Like a Pro

Sanjeev SharmaSanjeev Sharma
5 min read

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_token

Import 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 level

init.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_token

Useful 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 support

Creating 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__.py in 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__.py to control your public API
  • Avoid circular imports by moving shared types to a separate types.py or models.py
  • Keep __init__.py files thin — just re-exports, no business logic
  • Use pyproject.toml instead of the old setup.py for new packages

Key Takeaways

  • A module is any .py file; 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__.py defines the package's public API by controlling what is re-exported
  • Circular imports are a design smell — restructure by introducing a shared module
  • pyproject.toml is the modern standard for defining installable Python packages
  • __all__ controls what is exported when from module import * is used

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading