On this page

Modules and Packages

12 min read TextCh. 3 — Intermediate Python

Modules and Packages

As your programs grow, putting all the code in a single file becomes unmanageable. Python's module and package system lets you split code into logical units, reuse code across projects, and leverage the vast ecosystem of third-party libraries. Understanding how Python finds and loads modules is also essential for debugging import errors.

What Is a Module?

A module is simply a .py file. Any Python file can be imported as a module. When you write import math, Python finds the file math.py (or a compiled equivalent) and makes its contents available under the name math.

# math is a standard library module
import math

print(math.pi)           # 3.141592653589793
print(math.e)            # 2.718281828459045
print(math.sqrt(144))    # 12.0
print(math.floor(3.7))   # 3
print(math.ceil(3.2))    # 4
print(math.log(100, 10)) # 2.0 — log base 10 of 100
print(math.gcd(48, 18))  # 6 — greatest common divisor
print(math.factorial(10)) # 3628800

Import Styles

Python offers several ways to import names:

# 1. Import the entire module — access via module.name
import os
import sys
import json

print(os.getcwd())         # current working directory
print(sys.version)         # Python version string
print(sys.platform)        # 'linux', 'darwin', 'win32'

# 2. Import specific names — no module prefix needed
from math import sqrt, pi, tau
print(sqrt(16))   # 4.0
print(pi)         # 3.141592653589793

# 3. Import with alias — great for long module names
import datetime as dt
import collections as col

now = dt.datetime.now()
counter = col.Counter()

# 4. Import specific name with alias
from pathlib import Path as P
home = P.home()
print(home)

# 5. Import all public names (avoid in production code!)
from math import *
print(sin(pi / 2))   # 1.0 — but pollutes namespace

tip type: warning title: "Avoid wildcard imports in production"

from module import * imports all public names into your current namespace, potentially overwriting names that already exist. It makes it impossible to tell where a name came from just by reading the code. Reserve it for interactive exploration in the REPL — never use it in production code.

Creating Your Own Modules

Any .py file is a module. Here is how a project might be organized:

my_project/
├── main.py
├── utils/
│   ├── __init__.py
│   ├── strings.py
│   └── numbers.py
└── models/
    ├── __init__.py
    └── user.py

utils/strings.py:

def slugify(text: str) -> str:
    """Convert a string to a URL-friendly slug."""
    return text.lower().strip().replace(" ", "-")

def truncate(text: str, max_len: int = 100, suffix: str = "...") -> str:
    """Truncate text to max_len characters."""
    if len(text) <= max_len:
        return text
    return text[:max_len - len(suffix)] + suffix

def title_case(text: str) -> str:
    """Convert text to title case."""
    return " ".join(word.capitalize() for word in text.split())

main.py:

from utils.strings import slugify, truncate

title = "Hello World This is a Long Title"
print(slugify(title))        # hello-world-this-is-a-long-title
print(truncate(title, 20))   # Hello World This...

The `__init__.py` File

A directory with an __init__.py file is a package. The __init__.py can be empty (just marking the directory as a package) or can export a public API:

# utils/__init__.py — re-export commonly used names
from .strings import slugify, truncate, title_case
from .numbers import clamp, round_to

# Now users can do: from utils import slugify
# instead of: from utils.strings import slugify

The dot in .strings means "relative import" — import from the current package. This is the correct way to import within a package.

The `if __name__ == "__main__"` Guard

When Python runs a file directly, it sets __name__ to "__main__". When a file is imported as a module, __name__ is set to the module's name. This lets you write code that only runs when the file is executed directly:

# calculator.py

def add(a: float, b: float) -> float:
    return a + b

def subtract(a: float, b: float) -> float:
    return a - b

def multiply(a: float, b: float) -> float:
    return a * b

def divide(a: float, b: float) -> float:
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

if __name__ == "__main__":
    # This block only runs when you execute: python calculator.py
    # It does NOT run when another file does: import calculator
    print(add(10, 5))       # 15
    print(divide(10, 3))    # 3.3333...

This pattern is ubiquitous in Python — always add it to scripts that also contain importable functions.

`pip` — Python's Package Installer

pip is the standard package manager for Python. It downloads packages from PyPI (the Python Package Index):

# Install a package
pip install requests

# Install a specific version
pip install requests==2.31.0

# Install with version constraints
pip install "requests>=2.28,<3.0"

# Install from a requirements file
pip install -r requirements.txt

# Uninstall a package
pip uninstall requests

# List installed packages
pip list

# Show info about a specific package
pip show requests

# Freeze current environment to requirements.txt
pip freeze > requirements.txt

# Check for outdated packages
pip list --outdated

# Upgrade a package
pip install --upgrade requests

Virtual Environments

A virtual environment is an isolated Python installation that has its own packages, separate from your system Python. This prevents version conflicts between projects:

# Create a virtual environment in a folder called .venv
python -m venv .venv

# Activate the virtual environment
# On Windows (Command Prompt):
.venv\Scripts\activate.bat
# On Windows (PowerShell):
.venv\Scripts\Activate.ps1
# On macOS/Linux:
source .venv/bin/activate

# You'll see (.venv) in your prompt
(.venv) $ pip install requests fastapi

# Deactivate when done
deactivate

# Create requirements.txt from current environment
pip freeze > requirements.txt

# On a new machine, recreate the environment
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

tip type: tip title: "Always use virtual environments"

Never install packages globally (without a virtual environment) for project work. Always create a .venv inside your project folder and activate it. This way, each project has its own isolated set of dependencies and you avoid the notorious "it works on my machine" problem. Add .venv/ to your .gitignore.

Useful Standard Library Modules

Python's standard library is enormous. Here are the modules you will use most often:

`os` and `pathlib`

import os
from pathlib import Path

# pathlib (modern, object-oriented — preferred)
cwd = Path.cwd()
home = Path.home()
config = home / ".config" / "myapp" / "config.json"

print(config)
print(config.parent)      # ~/.config/myapp
print(config.name)        # config.json
print(config.stem)        # config
print(config.suffix)      # .json
print(config.exists())    # True or False

# Create directories
(home / "temp_test_dir").mkdir(parents=True, exist_ok=True)

# List files matching a pattern
py_files = list(Path(".").glob("**/*.py"))

`sys`

import sys

print(sys.argv)          # command-line arguments
print(sys.path)          # where Python looks for modules
print(sys.version_info)  # (3, 14, 0, 'final', 0)

# Exit the program
if len(sys.argv) < 2:
    print("Usage: script.py <filename>")
    sys.exit(1)          # non-zero = error

`datetime`

from datetime import datetime, date, timedelta, timezone

now = datetime.now(tz=timezone.utc)  # always use timezone-aware datetimes
today = date.today()

print(now.isoformat())    # 2025-04-02T14:30:00+00:00
print(today)              # 2025-04-02

# Arithmetic
one_week = timedelta(weeks=1)
next_week = today + one_week
print(next_week)          # 2025-04-09

# Formatting
print(now.strftime("%B %d, %Y"))   # April 02, 2025

# Parsing
deadline = datetime.strptime("2025-12-31", "%Y-%m-%d")
days_left = (deadline.date() - today).days
print(f"{days_left} days until deadline")

`random`

import random

# Reproducible randomness (for testing)
random.seed(42)

print(random.randint(1, 100))           # integer between 1 and 100 inclusive
print(random.uniform(0.0, 1.0))         # float between 0.0 and 1.0
print(random.choice(["a", "b", "c"]))   # pick one item
print(random.choices(["a", "b", "c"], k=5))  # pick k items with replacement

items = list(range(10))
random.shuffle(items)            # shuffle in place
print(items)

sample = random.sample(items, k=3)  # pick k unique items
print(sample)

`re` — Regular Expressions

import re

text = "Contact us at [email protected] or [email protected]"

# Find all email addresses
emails = re.findall(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", text)
print(emails)   # ['[email protected]', '[email protected]']

# Replace phone numbers
message = "Call 555-1234 or 555-5678 for info"
cleaned = re.sub(r"\d{3}-\d{4}", "[REDACTED]", message)
print(cleaned)  # Call [REDACTED] or [REDACTED] for info

# Validate a pattern
pattern = re.compile(r"^\d{4}-\d{2}-\d{2}$")   # YYYY-MM-DD
print(bool(pattern.match("2025-04-02")))  # True
print(bool(pattern.match("April 2")))     # False

nextSteps

  • classes-and-objects