Skip to main content

πŸ“¦ Python Modules and Packages

Real applications are thousands of lines spread across many files. Modules and packages are how Python keeps that manageable β€” letting you split code into reusable units, tap a huge standard library, and pull in community packages with a single command.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Create a module and import it four different ways
  • Explain the module search path and the role of if __name__ == "__main__"
  • Use key standard-library modules (os, datetime, json)
  • Build a package with __init__.py and clean re-exports
  • Install third-party packages with pip inside a virtual environment

Estimated Time: 40–50 minutes  β€’  Difficulty: Beginner–Intermediate

Hands-on: Structure a small data_validator package and set up an isolated venv.

In This Lesson

Why Modular Code?

Imagine building a house where you had to forge every nail and cast every window from scratch. Construction works because it assembles standardized, pre-made components. Python's modules and packages bring the same efficiency to code, letting you:

  • Organize related code into separate files and folders
  • Reuse code across many projects
  • Hide implementation details behind a clean interface
  • Share functionality with the wider Python community

Without this structure, a large backend would be an unnavigable single file. With it, teams split work across modules and each piece stays focused and testable.

Creating & Importing Modules

A module is simply a .py file. Its name is the filename without the extension. Let's build one called calculator.py:

# calculator.py
"""A tiny calculator module with basic arithmetic."""

def add(a, b):
    """Return the sum of a and b."""
    return a + b

def divide(a, b):
    """Return a / b, raising on division by zero."""
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

PI = 3.14159

Now you can import that functionality four different ways, each suited to a different situation:

# 1. Import the whole module (clearest β€” names stay namespaced)
import calculator
calculator.add(5, 3)
print(calculator.PI)

# 2. Import specific names (concise for a few items)
from calculator import add, PI
add(10, 7)

# 3. Import with an alias (handy for long names, e.g. numpy as np)
import calculator as calc
calc.divide(20, 4)

# 4. Import everything (discouraged β€” pollutes the namespace)
from calculator import *   # avoid: unclear where names come from
flowchart LR A[calculator.py] --> B["import calculator
β†’ calculator.add()"] A --> C["from calculator import add
β†’ add()"] A --> D["import calculator as calc
β†’ calc.add()"]

βœ… Prefer explicit imports

import calculator or from calculator import add make it obvious where a name comes from. Avoid from module import * in real code β€” it hides the source of every name and invites collisions.

The Search Path & __main__

When you write import calculator, Python hunts for the module in a specific order, stored in sys.path:

  1. The directory of the script being run (or the current directory)
  2. The standard-library directories
  3. The site-packages directory (where pip installs third-party packages)
  4. Any directories listed in the PYTHONPATH environment variable
import sys
for path in sys.path:
    print(path)

This mirrors how your operating system uses PATH to find executables β€” it checks a list of locations in order and uses the first match.

The if __name__ == "__main__" guard

Every module has a built-in __name__ variable. When a file is run directly, __name__ is "__main__"; when it is imported, __name__ is the module's name. This lets a file act as both a reusable library and a runnable script:

# greet.py
def greet(name):
    return f"Hello, {name}!"

if __name__ == "__main__":
    # Runs only when you do `python greet.py`, NOT when imported
    print(greet("World"))

πŸ’‘ Why it matters

Without the guard, any code at the bottom of a module β€” like a test print or a server start β€” would fire the moment another file imports it. The guard keeps "run me" logic separate from "import me" logic.

The Standard Library

Python ships with a rich standard library β€” "batteries included." These modules come with every install, no download required.

ModulePurposeCommon use
os / pathlibFilesystem & OS interfacePaths, files, environment variables
sysInterpreter internalsArguments, exit codes, sys.path
datetimeDates & timesTimestamps, durations, formatting
jsonJSON encode/decodeAPI payloads, config files
reRegular expressionsValidation, parsing, search
collectionsSpecialized containersCounter, defaultdict, namedtuple

A few in action

# Paths done right β€” pathlib is the modern choice
from pathlib import Path

cwd = Path.cwd()
config = Path("config") / "settings.ini"   # OS-independent joining
Path("new_folder").mkdir(exist_ok=True)
# Dates and durations
from datetime import datetime, timedelta

now = datetime.now()
print(now.strftime("%Y-%m-%d %H:%M:%S"))
tomorrow = now + timedelta(days=1)
# JSON β€” the lingua franca of web APIs
import json

user = {"name": "Alice", "age": 28, "active": True}
text = json.dumps(user, indent=2)     # Python dict -> JSON string
back = json.loads(text)               # JSON string -> Python dict
print(back["name"])                   # Alice

The standard library is a giant toolbox that comes free with Python β€” reach for it before installing anything.

Packages & __init__.py

A package is a directory of modules. Historically it's marked by an __init__.py file (which can be empty). Packages let you group related modules and namespace them hierarchically.

webapp/                  # top-level package
    __init__.py
    config.py
    models/              # sub-package
        __init__.py
        user.py
        product.py
    controllers/
        __init__.py
        auth.py
    utils/
        __init__.py
        validation.py
A Python package tree The webapp package branches into a config module and three sub-packages: models, controllers, and utils, each containing their own modules. webapp/ config.py models/ controllers/ utils/ user.py product.py
Figure 1 β€” A package is a folder tree of modules. Each folder's __init__.py marks it as importable and can re-export names for convenience.

What __init__.py is for

Beyond marking a folder as a package, __init__.py can run setup code and β€” most usefully β€” re-export names so callers get a clean, shallow import path:

# webapp/models/__init__.py
"""Data models for the app."""

from .user import User, Role
from .product import Product

__all__ = ["User", "Role", "Product"]   # controls `from models import *`
# Because of the re-exports above, callers can write:
from webapp.models import User, Product
# instead of the deeper:
from webapp.models.user import User
from webapp.models.product import Product

πŸ“– Relative vs absolute imports

Inside a package, from .user import User is a relative import (the dot means "this package"). From outside, use the absolute path from webapp.models import User. Prefer absolute imports in application code; relative imports are handy within a package's own internals.

pip & Virtual Environments

Python's real superpower is PyPI, a repository of hundreds of thousands of community packages. You install them with pip.

# Install a package
pip install requests

# Pin an exact version
pip install "flask==3.0.0"

# Install everything a project needs
pip install -r requirements.txt

# Snapshot your current dependencies
pip freeze > requirements.txt

Isolate every project with a virtual environment

Different projects need different (often conflicting) package versions. A virtual environment gives each project its own private set of packages β€” like separate apartments in a building, each furnished independently.

# Create a venv in a folder named .venv
python -m venv .venv

# Activate it
source .venv/bin/activate      # macOS / Linux
.venv\Scripts\activate         # Windows

# Now pip installs land inside this project only
pip install django

# When you're done
deactivate

⚠️ Never install into the system Python

Installing packages globally leads to version clashes between projects and can break OS tooling that depends on the system Python. Create and activate a virtual environment first, every time. Add .venv/ to your .gitignore.

Third-party vs standard library β€” a quick comparison

# Standard library β€” works everywhere, more verbose
import urllib.request, json
with urllib.request.urlopen("https://api.github.com/users/python") as r:
    data = json.loads(r.read().decode())

# The 'requests' package β€” install once, far cleaner
import requests
data = requests.get("https://api.github.com/users/python").json()
print(data["name"])

Structuring a Real Project

Here is how modules and packages come together in a realistic Flask backend. Notice how each concern lives in its own place:

myapp/
    __init__.py          # application factory (create_app)
    config.py            # settings per environment
    models/              # database models
    routes/              # request handlers (blueprints)
    services/            # email, analytics, etc.
    templates/           # HTML
    static/              # CSS, JS, images
app.py                   # entry point
requirements.txt         # dependencies
# myapp/__init__.py β€” the "application factory" pattern
from flask import Flask
from . import config

def create_app(config_name="default"):
    """Build and configure the Flask app, then return it."""
    app = Flask(__name__)
    app.config.from_object(getattr(config, f"{config_name.capitalize()}Config"))

    # Register route blueprints
    from .routes import auth, blog
    app.register_blueprint(auth.bp)
    app.register_blueprint(blog.bp)

    return app
# app.py β€” the entry point
from myapp import create_app

app = create_app()

if __name__ == "__main__":
    app.run(debug=True)

βœ… Why this structure wins

  • Each part is small, focused, and easy to locate.
  • Multiple developers can work on different sub-packages without collisions.
  • Isolated modules are far easier to unit-test.
  • Code is portable β€” a well-factored utils package drops into the next project.

Hands-on Exercise

πŸ‹οΈ Build a data_validator Package

Objective: Turn loose validation functions into a proper, importable package inside a virtual environment.

Instructions:

  1. Create and activate a virtual environment (python -m venv .venv).
  2. Make this folder tree:
    data_validator/
        __init__.py
        email.py
        password.py
  3. In email.py, write validate_email(addr) using the re module; in password.py, write password_strength(pw).
  4. In __init__.py, re-export both so callers can do from data_validator import validate_email.
  5. Write a demo.py beside the package that imports and exercises it.
πŸ’‘ Hint

A serviceable email regex is r"^[^@\s]+@[^@\s]+\.[^@\s]+$". In __init__.py, use relative imports: from .email import validate_email. Run the demo from the folder containing data_validator/ so the package is on the search path.

βœ… Sample solution
# data_validator/email.py
import re
_EMAIL = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")

def validate_email(addr):
    """Return True if addr looks like a valid email address."""
    return isinstance(addr, str) and bool(_EMAIL.match(addr.strip()))


# data_validator/password.py
def password_strength(pw):
    """Return a 0–4 score based on simple strength rules."""
    checks = [
        len(pw) >= 8,
        any(c.isupper() for c in pw),
        any(c.isdigit() for c in pw),
        any(not c.isalnum() for c in pw),
    ]
    return sum(checks)


# data_validator/__init__.py
"""Reusable validators for web input."""
from .email import validate_email
from .password import password_strength
__all__ = ["validate_email", "password_strength"]


# demo.py
from data_validator import validate_email, password_strength
print(validate_email("user@example.com"))   # True
print(password_strength("Secret123!"))       # 4

🎯 Quick Quiz

Question 1: What makes a directory a Python package in the traditional sense?

Question 2: When you run python greet.py directly, what is the value of __name__ in that file?

Question 3: Why should each project use its own virtual environment?

Summary & Quiz

πŸŽ‰ Key Takeaways

  • A module is a .py file; import it whole, selectively, or with an alias β€” avoid import *.
  • Python finds modules via sys.path; the if __name__ == "__main__" guard separates "run me" from "import me".
  • The standard library (os/pathlib, datetime, json, re, collections) covers most everyday needs with zero installs.
  • A package is a folder of modules; __init__.py marks it and can re-export a clean public API.
  • Install third-party packages with pip inside a per-project virtual environment.

πŸ“š Further Reading

πŸš€ What's Next?

You've now covered Python end to end β€” syntax, control flow, and organization. Next we switch languages to see the same backend ideas in a different light, starting with PHP syntax and variables.

πŸŽ‰ Well organized!

You can now split, share, and reuse Python code like a pro. On to PHP.