Modules, Packages & pip
Modules, Packages & pip: Organizing Code and Using the Python Ecosystem
Python's module system lets you split code into reusable files and use thousands of third-party libraries. Understanding it properly prevents the import errors and dependency conflicts that trip up beginners.
Modules: Single Files
Any .py file is a module. You import it to use its contents.
# math_utils.py
PI = 3.14159265358979
def circle_area(radius):
return PI * radius ** 2
def circle_circumference(radius):
return 2 * PI * radius
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return circle_area(self.radius)
# main.py — in the same directory
import math_utils
print(math_utils.PI)
print(math_utils.circle_area(5))
# Selective import
from math_utils import circle_area, PI
# Alias import
import math_utils as mu
print(mu.circle_area(5))
# Import everything (usually avoid this — pollutes namespace)
from math_utils import *
The if __name__ == '__main__' Pattern
Every module has a __name__ attribute. When run directly, __name__ == '__main__'. When imported, __name__ is the module name.
# calculator.py
def add(a, b):
return a + b
def multiply(a, b):
return a * b
def main():
print("Running calculator demo:")
print(f"3 + 4 = {add(3, 4)}")
print(f"3 × 4 = {multiply(3, 4)}")
if __name__ == '__main__':
# Only runs when this file is executed directly
# NOT when imported as a module
main()
# When you run: python calculator.py → main() executes
# When you run: import calculator → main() does NOT execute
This pattern is essential — without it, importing your module would run all its code.
Packages: Directories of Modules
A package is a directory containing an __init__.py file (can be empty).
myapp/
├── __init__.py ← Makes myapp a package
├── config.py
├── models/
│ ├── __init__.py
│ ├── user.py
│ └── product.py
├── services/
│ ├── __init__.py
│ ├── auth.py
│ └── payment.py
└── utils/
├── __init__.py
└── helpers.py
# Importing from packages
from myapp.models.user import User
from myapp.services.auth import authenticate
import myapp.config as config
# __init__.py can re-export for cleaner imports
# myapp/models/__init__.py
from .user import User
from .product import Product
# Now users can do:
from myapp.models import User, Product
# Instead of:
from myapp.models.user import User
Relative vs Absolute Imports
# myapp/services/auth.py
# Absolute import (always works, explicit)
from myapp.models.user import User
from myapp.config import SECRET_KEY
# Relative import (relative to current package)
from ..models.user import User # Go up one level, then into models
from ..config import SECRET_KEY # Go up one level, get config
from . import helpers # Import from same package
Within a package, relative imports are common. In scripts, always use absolute imports.
pip: Installing Third-Party Packages
# Install a package
pip install requests
# Install specific version
pip install requests==2.31.0
# Install minimum version
pip install "requests>=2.25.0"
# Upgrade a package
pip install --upgrade requests
# Uninstall
pip uninstall requests
# List installed packages
pip list
# Show package info and dependencies
pip show requests
# Save current environment's packages
pip freeze > requirements.txt
# Install from requirements file
pip install -r requirements.txt
requirements.txt: Sharing Dependencies
# requirements.txt
requests==2.31.0
pandas>=2.0.0
numpy>=1.24.0
scikit-learn>=1.3.0
python-dotenv==1.0.0
fastapi>=0.100.0
uvicorn[standard]>=0.23.0
Always pin exact versions for production deployments. Use >= ranges for libraries you develop.
The Standard Library: Batteries Included
Python ships with an enormous standard library. Know what's available before installing third-party packages:
# pathlib — file paths (covered in File I/O lesson)
# json — JSON parsing
# csv — CSV reading/writing
# datetime — date and time handling
# re — regular expressions
# math — mathematical functions
# random — random number generation
# collections — specialized containers
# itertools — functional tools for iterables
# functools — higher-order functions
# os, sys — OS interface and system info
# subprocess — run external commands
# logging — logging (covered in error handling)
# typing — type hints
# dataclasses — data classes
# abc — abstract base classes
# threading, asyncio — concurrency
# unittest — testing framework
# argparse — command-line arguments
# hashlib — hashing (SHA, MD5)
# base64 — encoding
# time, timeit — timing and performance
# Examples
from datetime import datetime, timedelta
import random
from collections import Counter, defaultdict, deque
import itertools
# Counter — count occurrences
words = "the quick brown fox jumps over the lazy dog".split()
word_count = Counter(words)
print(word_count.most_common(3)) # [('the', 2), ('quick', 1), ('brown', 1)]
# deque — efficient queue
queue = deque(maxlen=5) # Fixed-size queue
for i in range(10):
queue.append(i)
print(queue) # deque([5, 6, 7, 8, 9], maxlen=5)
# itertools
# Infinite counter
counter = itertools.count(10, 2) # 10, 12, 14, ...
# Combinations
for combo in itertools.combinations("ABC", 2):
print(combo) # ('A', 'B'), ('A', 'C'), ('B', 'C')
# Chain iterables
from itertools import chain
numbers = list(chain([1, 2], [3, 4], [5, 6]))
print(numbers) # [1, 2, 3, 4, 5, 6]
Essential Third-Party Libraries
# requests — HTTP requests
import requests
response = requests.get('https://api.github.com/users/python')
user = response.json()
# pandas — data analysis
import pandas as pd
df = pd.read_csv('data.csv')
print(df.head())
# numpy — numerical computing
import numpy as np
arr = np.array([1, 2, 3, 4, 5])
print(arr.mean(), arr.std())
# python-dotenv — load .env files
from dotenv import load_dotenv
import os
load_dotenv()
api_key = os.getenv('API_KEY')
# rich — beautiful terminal output
from rich import print
from rich.table import Table
print("[bold green]Hello, World![/bold green]")
# pydantic — data validation
from pydantic import BaseModel, validator
class User(BaseModel):
name: str
age: int
email: str
Namespace Packages (Python 3.3+)
A directory without __init__.py is a namespace package — useful for large codebases split across multiple directories.
org/
├── team_a/
│ └── module_a.py # No __init__.py needed!
└── team_b/
└── module_b.py
# From separate locations, both are importable
from org.team_a.module_a import something
from org.team_b.module_b import other_thing
Finding Where a Module Comes From
import requests
import numpy as np
# Where is this module located?
print(requests.__file__) # Path to requests package directory
print(np.__file__) # Path to numpy
# What version?
print(requests.__version__)
print(np.__version__)
# What's in a module?
import math
print(dir(math)) # All attributes
# What does a function do?
help(math.log)
print(math.log.__doc__) # Docstring
Next lesson: Virtual Environments & Project Setup — keeping dependencies isolated.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises