Chapter 15: Modules and Packages
Every program you've written so far lives in one file. That works for small programs. But as programs grow — hundreds of lines, then thousands — one file becomes impossible to navigate, understand, or maintain.
Python's answer is modules and packages: a way to split code across multiple files, import what you need, and build on the work of millions of other programmers through the standard library and third-party packages.
This chapter teaches you how Python's import system works, how to write your own modules, how to structure a package, how to install third-party libraries with pip, and how to manage environments so different projects don't interfere with each other.
What is a Module?
A module is simply a Python file. Any .py file you write is a module. Its name is the filename without the .py extension.
When you write:
import math
Python finds a file called math.py somewhere on your system, runs it, and makes everything defined in it available to you as math.something.
You've been using modules since Chapter 3 (import math) and Chapter 9 (from datetime import datetime). Now you'll understand exactly what's happening.
Three Ways to Import
import module — Import the whole module
import math
print(math.pi) # 3.141592653589793
print(math.sqrt(16)) # 4.0
print(math.ceil(4.2)) # 5
You access everything through the module name. This keeps namespaces clean — math.sqrt is unambiguous.
from module import name — Import specific names
from math import pi, sqrt, ceil
print(pi) # 3.141592653589793
print(sqrt(16)) # 4.0
print(ceil(4.2))# 5
The names are imported directly into your namespace — no math. prefix needed. Useful when you use the same function constantly and the shorter name is clear.
Be careful: if you import something that clashes with a name you already defined, the import wins (or your definition wins, depending on order). Keep imports at the top of the file where they're visible.
import module as alias — Import with a shorter name
import numpy as np # convention in data science
import pandas as pd # convention
import matplotlib.pyplot as plt
# Now use the alias
arr = np.array([1, 2, 3])
Aliases are used throughout the Python ecosystem for popular libraries. np, pd, plt are so universal that code using them is immediately recognizable to any data scientist.
You can alias specific imports too:
from collections import defaultdict as dd
from datetime import datetime as dt
now = dt.now()
from module import * — Import everything (avoid this)
from math import * # imports everything math defines
print(sqrt(16)) # works, but...
This pollutes your namespace with dozens of names you may not expect. If two modules both define sqrt, one silently overwrites the other. Use it only in interactive sessions for quick exploration, never in production code.
How Python Finds Modules
When you write import something, Python searches these locations in order:
- The current directory — your script's folder
PYTHONPATH— an environment variable listing extra directories- The standard library — Python's built-in modules
- Site-packages — where
pipinstalls third-party packages
You can see the full search path:
import sys
print(sys.path)
This list of directories is where Python looks. If your module isn't in any of them, you get a ModuleNotFoundError.
Writing Your Own Module
Any .py file is a module. Create a file called greetings.py:
# greetings.py
DEFAULT_GREETING = "Hello"
def greet(name, greeting=DEFAULT_GREETING):
"""Return a greeting string."""
return f"{greeting}, {name}!"
def farewell(name):
"""Return a farewell string."""
return f"Goodbye, {name}. See you soon!"
def greet_all(names, greeting=DEFAULT_GREETING):
"""Return a list of greeting strings."""
return [greet(name, greeting) for name in names]
Now in another file in the same folder, import it:
# main.py
import greetings
print(greetings.greet("Alice"))
print(greetings.farewell("Bob"))
print(greetings.DEFAULT_GREETING)
messages = greetings.greet_all(["Carlos", "Diana", "Eve"])
for msg in messages:
print(msg)
Output:
Hello, Alice!
Goodbye, Bob. See you soon!
Hello
Hello, Carlos!
Hello, Diana!
Hello, Eve!
That's it. A module is just a file with functions, variables, and classes. Import it by name and use what's inside.
if __name__ == "__main__":
This is one of the most important Python idioms. Every module has a special variable called __name__. When a file is run directly, __name__ equals "__main__". When a file is imported, __name__ equals the module's name.
# greetings.py
DEFAULT_GREETING = "Hello"
def greet(name, greeting=DEFAULT_GREETING):
return f"{greeting}, {name}!"
def farewell(name):
return f"Goodbye, {name}. See you soon!"
# This block only runs when the file is executed directly
# It does NOT run when the module is imported
if __name__ == "__main__":
print("Running greetings module directly.")
print(greet("World"))
print(farewell("World"))
Run python greetings.py directly -> the if __name__ block runs.
Import it from main.py -> it doesn't run.
This pattern lets you write modules that are also runnable scripts. Put your tests, demos, or CLI entry point inside if __name__ == "__main__": and the module stays importable without side effects.
Your turn: Create a module called calculator.py with four functions: add, subtract, multiply, divide. Add a if __name__ == "__main__": block that tests each function and prints the results. Import the module from a second file and use all four functions.
What is a Package?
A package is a folder containing multiple modules, plus a special file called __init__.py.
myproject/
├── main.py
└── utils/
├── __init__.py
├── math_tools.py
├── string_tools.py
└── file_tools.py
The __init__.py file tells Python "this folder is a package." It can be empty, or it can define what gets exported when someone does import utils.
# utils/math_tools.py
def square(n):
return n ** 2
def cube(n):
return n ** 3
# utils/string_tools.py
def titlecase(text):
return text.title()
def word_count(text):
return len(text.split())
# main.py
from utils import math_tools
from utils.string_tools import titlecase, word_count
print(math_tools.square(5)) # 25
print(math_tools.cube(3)) # 27
print(titlecase("hello world")) # Hello World
print(word_count("one two three")) # 3
Controlling what import utils exports
In utils/__init__.py, you can import the things you want to expose at the package level:
# utils/__init__.py
from .math_tools import square, cube
from .string_tools import titlecase, word_count
Now users can do:
from utils import square, titlecase
Without needing to know which submodule each function lives in. This is how libraries like requests and pandas work — you import from the top-level package, not the internal submodules.
The Standard Library
Python ships with a massive standard library — hundreds of modules covering almost everything you'd need. You don't need to install anything; they're included with Python.
Here's a taste of what's available:
| Module | What it does |
|---|---|
math |
Mathematical functions and constants |
os |
Operating system interface — files, paths, environment |
sys |
Python interpreter — version, path, argv |
datetime |
Dates, times, timedeltas |
random |
Random numbers and choices |
json |
Read and write JSON |
csv |
Read and write CSV |
re |
Regular expressions |
pathlib |
Modern file path handling |
collections |
Counter, defaultdict, deque, OrderedDict, namedtuple |
itertools |
Powerful iteration tools |
functools |
Higher-order functions — lru_cache, reduce, partial |
string |
String constants and Template |
time |
Time access and conversions |
hashlib |
Secure hash functions |
urllib |
URL handling and HTTP requests (basic) |
sqlite3 |
SQLite database |
logging |
Logging framework |
unittest |
Testing framework |
argparse |
Command-line argument parsing |
threading |
Thread-based parallelism |
multiprocessing |
Process-based parallelism |
asyncio |
Async I/O |
abc |
Abstract base classes |
dataclasses |
Dataclass decorator |
typing |
Type hints |
contextlib |
Context manager utilities |
copy |
Shallow and deep copy |
io |
In-memory file-like objects |
struct |
Binary data packing/unpacking |
socket |
Low-level networking |
http |
HTTP server and client |
email |
Email composition and parsing |
zipfile |
ZIP archive reading/writing |
tarfile |
TAR archive reading/writing |
shutil |
High-level file operations |
tempfile |
Temporary files and directories |
subprocess |
Run shell commands |
signal |
Signal handling |
traceback |
Print and inspect tracebacks |
Chapter 16 covers the most useful ones in detail.
Installing Third-Party Packages with pip
The standard library is huge, but the real Python ecosystem is the Python Package Index (PyPI) — over 500,000 packages built by the community. pip is the tool that installs them.
Installing a package
Open your terminal and run:
pip install requests
After installation, import it normally:
import requests
response = requests.get("https://api.github.com")
print(response.status_code) # 200
print(response.json()["current_user_url"])
Installing a specific version
pip install requests==2.31.0
Upgrading a package
pip install --upgrade requests
Uninstalling a package
pip uninstall requests
Listing installed packages
pip list
Seeing where a package is installed
pip show requests
requirements.txt — Sharing Your Environment
When you share your project with someone else (or deploy it to a server), they need to know which packages to install and which versions you used. The standard way is a requirements.txt file.
Creating it
pip freeze > requirements.txt
This writes all installed packages and their exact versions to requirements.txt:
certifi==2024.2.2
charset-normalizer==3.3.2
idna==3.6
requests==2.31.0
urllib3==2.2.0
Installing from it
pip install -r requirements.txt
This installs every package listed. One command restores your entire environment.
Virtual Environments — Isolating Projects
Here's a problem: Project A needs requests==2.25.0. Project B needs requests==2.31.0. If you install both, one overwrites the other. Your system Python can only hold one version of any package at a time.
A virtual environment solves this. It's an isolated Python installation for one project — its own copy of Python, its own packages, completely separate from your system and every other project.
Creating a virtual environment
python -m venv venv
This creates a folder called venv in your current directory containing a full isolated Python installation.
Activating it
Windows:
venv\Scripts\activate
Mac/Linux:
source venv/bin/activate
After activation, your terminal prompt changes to show (venv). Every pip install now installs into this environment only.
Using it
(venv) pip install requests flask pandas
(venv) pip freeze > requirements.txt
(venv) python main.py
Deactivating it
deactivate
The standard project layout
my_project/
├── venv/ <- virtual environment (NEVER commit to git)
├── .gitignore <- include venv/ here
├── requirements.txt <- commit this
├── README.md
├── main.py
└── utils/
├── __init__.py
└── helpers.py
Always add venv/ to .gitignore — it's hundreds of megabytes and specific to your machine. Commit requirements.txt instead so anyone who clones your project can recreate the environment with pip install -r requirements.txt.
Relative Imports
Inside a package, you can use relative imports to import from sibling modules without spelling out the full package path.
# utils/math_tools.py
# Absolute import — works from anywhere
from utils.string_tools import titlecase
# Relative import — works only inside the package
from .string_tools import titlecase # same package
from ..other_package import something # parent package
The . means "current package." .. means "parent package." Relative imports make refactoring easier — if you rename the package, relative imports inside it don't need to change.
__init__.py in Depth
In Python 3.3+, packages don't actually require __init__.py — Python will find the folder as a "namespace package" without it. But it's still best practice to include one because:
- It signals clearly that this folder is a Python package.
- It lets you control what the package exports.
- It lets you run initialization code when the package is imported.
# utils/__init__.py
# Package version
__version__ = "1.0.0"
__author__ = "Alice Smith"
# Expose the most important things at the top level
from .math_tools import square, cube
from .string_tools import titlecase, word_count
# Run once when the package is imported
print(f"utils package v{__version__} loaded.")
Putting It All Together: A Utility Package
Here's a complete mini-package structure:
weather_app/
├── main.py
├── requirements.txt
└── weather/
├── __init__.py
├── fetcher.py
├── parser.py
└── formatter.py
# weather/fetcher.py
import requests
def fetch_weather(city, api_key):
"""Fetch current weather data for a city."""
url = f"https://api.openweathermap.org/data/2.5/weather"
params = {"q": city, "appid": api_key, "units": "metric"}
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
return response.json()
# weather/parser.py
def parse_weather(data):
"""Extract key fields from raw API response."""
return {
"city": data["name"],
"country": data["sys"]["country"],
"temp": data["main"]["temp"],
"feels_like": data["main"]["feels_like"],
"humidity": data["main"]["humidity"],
"description": data["weather"][0]["description"].title(),
"wind_speed": data["wind"]["speed"],
}
# weather/formatter.py
def format_report(weather):
"""Format parsed weather data into a readable string."""
return (
f"Weather in {weather['city']}, {weather['country']}\n"
f" {weather['description']}\n"
f" Temperature: {weather['temp']}°C "
f"(feels like {weather['feels_like']}°C)\n"
f" Humidity: {weather['humidity']}%\n"
f" Wind speed: {weather['wind_speed']} m/s"
)
# weather/__init__.py
from .fetcher import fetch_weather
from .parser import parse_weather
from .formatter import format_report
__version__ = "1.0.0"
# main.py
from weather import fetch_weather, parse_weather, format_report
API_KEY = "your_api_key_here"
city = input("Enter city name: ")
try:
raw = fetch_weather(city, API_KEY)
parsed = parse_weather(raw)
print(format_report(parsed))
except Exception as e:
print(f"Could not get weather: {e}")
Each file does one job. main.py orchestrates. The package is importable by anyone. This is how real Python applications are structured.
What You Learned in This Chapter
- A module is any
.pyfile. Import it withimport name. - Three import styles:
import module,from module import name,import module as alias. from module import *pollutes your namespace — avoid it in production code.- Python searches for modules in: current directory ->
PYTHONPATH-> standard library -> site-packages. if __name__ == "__main__":runs only when a file is executed directly, not when imported.- A package is a folder with
__init__.pycontaining multiple modules. __init__.pycontrols what the package exposes and can run initialization code.- Relative imports use
.(current package) and..(parent package). - The standard library has hundreds of modules — check it before installing anything.
pip install packageinstalls from PyPI.pip freeze > requirements.txtsaves your dependencies.- Virtual environments (
python -m venv venv) isolate each project's packages. - Always add
venv/to.gitignore. Commitrequirements.txtinstead.
What's Next?
Now that you know how to import modules, Chapter 16 gives you a guided tour of the most useful ones in Python's standard library: os, sys, datetime, random, json, re, itertools, functools, and pathlib. You'll use these in almost every real project you write.
Your turn: Create a package called converters with three modules:
temperature.py— functions for Celsius ↔ Fahrenheit ↔ Kelvindistance.py— functions for km ↔ miles, m ↔ feetcurrency.py— a simple function that multiplies by an exchange rate
Create an __init__.py that exposes the most important functions at the package level. Write a main.py that imports from the package and lets the user pick a conversion type, enter a value, and see the result.