Skip to main content
Python beginner Lesson 16 of 28

Python Modules and Packages

Learn how to import modules, create packages, manage pip dependencies, and structure Python projects with virtual environments.

What Is a Module?

A module is any .py file. When you write import math, Python finds math.py (or a compiled equivalent) on sys.path and loads it.

# math is a standard library module
import math

math.sqrt(16)    # 4.0
math.pi          # 3.141592653589793
math.floor(3.7)  # 3

Import Styles

# Import the module — access via dotted name
import os
os.path.join("/home", "alice")

# Import specific names
from os.path import join, exists
join("/home", "alice")

# Import with alias — common for long names
import numpy as np
import pandas as pd
from datetime import datetime as dt

# Import all public names (avoid in production)
from math import *

Conditional Imports

try:
    import ujson as json     # fast C library
except ImportError:
    import json              # fallback to stdlib

The name Guard

# utils.py
def add(a, b):
    return a + b

if __name__ == "__main__":
    # Only runs when this file is executed directly
    # Not when imported as a module
    print(add(2, 3))

This pattern lets a file work as both a reusable module and a runnable script.

Creating a Package

A package is a directory with an __init__.py:

myapp/
├── __init__.py
├── auth/
│   ├── __init__.py
│   ├── tokens.py
│   └── passwords.py
├── api/
│   ├── __init__.py
│   ├── routes.py
│   └── middleware.py
└── utils.py

__init__.py can be empty or can define what gets exported:

# myapp/auth/__init__.py
from .tokens import generate_token, verify_token
from .passwords import hash_password, check_password

__all__ = ["generate_token", "verify_token", "hash_password", "check_password"]

Now users can do:

from myapp.auth import generate_token
# instead of:
from myapp.auth.tokens import generate_token

Relative Imports

Within a package, use relative imports to reference sibling modules:

# myapp/api/routes.py
from ..auth import verify_token          # go up one level to myapp, then into auth
from ..utils import format_response      # sibling in myapp
from .middleware import rate_limit       # sibling in myapp/api

Relative imports only work inside packages — they fail in top-level scripts.

sys.path and Module Discovery

Python searches for modules in this order:

  1. The directory of the script being run
  2. PYTHONPATH environment variable directories
  3. Standard library directories
  4. site-packages (where pip installs packages)
import sys
print(sys.path)

# Add a directory at runtime (avoid in production — use proper packaging)
sys.path.insert(0, "/path/to/my/libs")

pip: Installing Packages

# Install latest version
pip install requests

# Install pinned version
pip install "requests==2.31.0"

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

# Upgrade
pip install --upgrade requests

# Show what's installed
pip list
pip show requests

# Search PyPI
pip index versions requests

# Uninstall
pip uninstall requests

requirements.txt Best Practices

Pin exact versions for reproducibility in production:

# requirements.txt
requests==2.31.0
fastapi==0.110.0
pydantic==2.5.0
uvicorn==0.27.0

Use ranges only when you’re publishing a library (not an app):

# setup.cfg or pyproject.toml for a library
requests>=2.28,<3.0

Virtual Environments

Every project should have its own isolated environment:

# Create
python -m venv .venv

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

# Deactivate
deactivate

Modern Tooling: uv

uv is a drop-in replacement for pip + venv, written in Rust, 10–100x faster:

# Install uv
pip install uv

# Create venv and install
uv venv
uv pip install -r requirements.txt

# Or use uv's project management
uv init my-project
uv add requests fastapi
uv run python main.py

pyproject.toml (Modern Packaging)

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "my-app"
version = "1.0.0"
description = "A sample application"
requires-python = ">=3.11"
dependencies = [
    "requests>=2.31",
    "fastapi>=0.110",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "ruff>=0.3",
    "mypy>=1.8",
]

[tool.ruff]
line-length = 88

[tool.mypy]
strict = true

Install dev dependencies:

pip install -e ".[dev]"

Useful Standard Library Modules

import os           # file system, environment variables
import sys          # interpreter, argv, path
import pathlib      # object-oriented file paths
import json         # JSON encode/decode
import re           # regular expressions
import datetime     # dates and times
import collections  # Counter, defaultdict, deque, namedtuple
import itertools    # combinatorial generators
import functools    # lru_cache, partial, reduce
import contextlib   # context manager helpers
import logging      # production logging
import unittest     # test framework
import subprocess   # run shell commands
import threading    # threads
import multiprocessing  # processes
import asyncio      # async I/O
import socket       # networking
import http.client  # HTTP
import urllib.parse # URL manipulation
import hashlib      # SHA/MD5 hashing
import hmac         # HMAC authentication
import secrets      # cryptographic random
import tempfile     # temporary files
import shutil       # file operations (copy, move, rmtree)

Lazy Imports for Performance

Large imports slow down startup. Import inside functions when the module is only sometimes needed:

def export_to_excel(data):
    import openpyxl   # only imported when this function is called
    wb = openpyxl.Workbook()
    ...

Python caches modules in sys.modules after the first import, so subsequent calls are free.

Frequently Asked Questions

What's the difference between a module and a package?
A module is a single .py file. A package is a directory containing an __init__.py file and one or more modules.
When should I use 'from x import y' vs 'import x'?
Use 'import x' when you use multiple things from x or want the namespace clear. Use 'from x import y' for frequently used names in a module. Avoid 'from x import *' in production code.
What is __all__ used for?
__all__ is a list of names that get exported when someone does 'from module import *'. It also signals the public API of your module to tools and readers.