Modules and Packages

Organizing and reusing Python code.

Modules

A module is a single .py file:

# mymodule.py
"""Module docstring - describes what this module does."""

PI = 3.14159

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

class Calculator:
    def add(self, a, b):
        return a + b

Importing Modules

# Import entire module
import mymodule
mymodule.greet("Alice")
mymodule.PI

# Import with alias
import mymodule as mm
mm.greet("Alice")

# Import specific items
from mymodule import greet, PI
greet("Alice")

# Import with alias
from mymodule import greet as say_hello
say_hello("Alice")

# Import all (avoid - pollutes namespace)
from mymodule import *

Module Search Path

Python looks for modules in this order:

  1. Current directory
  2. PYTHONPATH environment variable
  3. Standard library
  4. Site-packages (installed packages)
import sys
print(sys.path)  # List of search directories

The name Variable

# mymodule.py
def main():
    print("Running as script")

if __name__ == "__main__":
    # Only runs when executed directly, not when imported
    main()
python mymodule.py    # Runs main()
import mymodule       # Does NOT run main()

Packages

A package is a directory containing modules:

mypackage/
├── __init__.py       # Makes it a package
├── module1.py
├── module2.py
└── subpackage/
    ├── __init__.py
    └── module3.py

init.py

Runs when package is imported. Use it to:

# mypackage/__init__.py

# Control what's exported with "from package import *"
__all__ = ['module1', 'useful_function']

# Import commonly used items for convenience
from .module1 import useful_function
from .module2 import UsefulClass

# Package-level constants
VERSION = "1.0.0"

Importing from Packages

# Import module from package
import mypackage.module1
mypackage.module1.function()

# Import specific item
from mypackage.module1 import function
function()

# Import from subpackage
from mypackage.subpackage import module3

# Import package (uses __init__.py exports)
import mypackage
mypackage.useful_function()

Relative Imports

Within a package, use relative imports:

# In mypackage/module2.py
from . import module1           # Same package
from .module1 import function   # Specific item
from ..otherpackage import x    # Parent's sibling

Relative imports only work within packages, not in scripts run directly.

Virtual Environments

Isolated Python environments for project dependencies:

Creating and Using

# Create virtual environment
python3 -m venv venv

# Activate (Linux/macOS)
source venv/bin/activate

# Activate (Windows)
venv\Scripts\activate

# Deactivate
deactivate

Managing Dependencies

# Install package
pip install requests

# Install specific version
pip install requests==2.28.0

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

# Export current packages
pip freeze > requirements.txt

# Show installed packages
pip list

# Show package info
pip show requests

# Upgrade package
pip install --upgrade requests

# Uninstall
pip uninstall requests

requirements.txt

requests==2.28.0
flask>=2.0,<3.0
numpy
pytest>=7.0

pyproject.toml (Modern)

[project]
name = "myproject"
version = "1.0.0"
dependencies = [
    "requests>=2.28",
    "flask>=2.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0",
    "black",
]

Installing Packages

pip

# From PyPI
pip install package_name

# From GitHub
pip install git+https://github.com/user/repo.git

# From local directory
pip install /path/to/package

# Editable install (for development)
pip install -e .

Common Packages

CategoryPackages
Webrequests, httpx, flask, fastapi, django
Datapandas, numpy, polars
Testingpytest, unittest
CLIclick, typer, argparse
Configpython-dotenv, pyyaml, toml
Databasesqlalchemy, psycopg2, sqlite3
Asyncasyncio, aiohttp, trio

Standard Library Highlights

Built-in modules - no installation needed:

# Operating system
import os
import sys
import shutil

# File paths
from pathlib import Path

# Data formats
import json
import csv
import configparser

# Date and time
import datetime
import time

# Math
import math
import random
import statistics

# Text
import re
import string
import textwrap

# Data structures
from collections import defaultdict, Counter, deque
from itertools import chain, groupby, combinations
from functools import reduce, partial, lru_cache

# Concurrency
import threading
import multiprocessing
import asyncio
import concurrent.futures

# Networking
import urllib.request
import socket
import http.server

# Testing
import unittest
import doctest

# Debugging
import pdb
import logging
import traceback

# Compression
import gzip
import zipfile
import tarfile

Creating Your Own Package

Basic Structure

myproject/
├── pyproject.toml
├── README.md
├── src/
│   └── mypackage/
│       ├── __init__.py
│       ├── core.py
│       └── utils.py
└── tests/
    ├── __init__.py
    └── test_core.py

pyproject.toml

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "mypackage"
version = "0.1.0"
description = "A short description"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
    "requests>=2.28",
]

[project.scripts]
mycommand = "mypackage.cli:main"

Installing Locally

# Install in development mode
pip install -e .

# Now you can import it anywhere
python -c "import mypackage"

Import Best Practices

Import Order

# 1. Standard library
import os
import sys
from pathlib import Path

# 2. Third-party packages
import requests
import pandas as pd

# 3. Local packages
from mypackage import utils
from .module import function

What to Avoid

# Avoid: importing inside functions (usually)
def process():
    import pandas  # Slow, hard to track dependencies

# Avoid: circular imports
# file_a.py imports file_b.py which imports file_a.py

# Avoid: import *
from module import *  # Pollutes namespace, unclear dependencies

# Avoid: shadowing standard library
# Don't name your file: email.py, random.py, etc.

Lazy Imports

For expensive imports in CLI tools:

def expensive_operation():
    import pandas as pd  # Only import when needed
    return pd.DataFrame()

Namespace Packages (Advanced)

Packages without __init__.py - can be split across directories:

# Two separate directories, same namespace
dir1/mynamespace/package_a/
dir2/mynamespace/package_b/

# Both accessible as:
from mynamespace import package_a
from mynamespace import package_b

Practice

# 1. Create a simple package structure
"""
calculator/
├── __init__.py
├── basic.py      # add, subtract, multiply, divide
└── advanced.py   # power, sqrt, factorial
"""

# calculator/__init__.py
from .basic import add, subtract
from .advanced import power

# calculator/basic.py
def add(a, b):
    return a + b

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

# Usage:
from calculator import add
add(2, 3)

# 2. Virtual environment workflow
"""
python3 -m venv venv
source venv/bin/activate
pip install requests
pip freeze > requirements.txt
"""

# 3. Check if module exists
import importlib.util
def module_exists(name):
    return importlib.util.find_spec(name) is not None

module_exists("requests")  # True or False