Python: Zero to Hero
Home/Python Essentials
Share

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:

  1. The current directory — your script's folder
  2. PYTHONPATH — an environment variable listing extra directories
  3. The standard library — Python's built-in modules
  4. Site-packages — where pip installs 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:

  1. It signals clearly that this folder is a Python package.
  2. It lets you control what the package exports.
  3. 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 .py file. Import it with import 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__.py containing multiple modules.
  • __init__.py controls 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 package installs from PyPI. pip freeze > requirements.txt saves your dependencies.
  • Virtual environments (python -m venv venv) isolate each project's packages.
  • Always add venv/ to .gitignore. Commit requirements.txt instead.

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 ↔ Kelvin
  • distance.py — functions for km ↔ miles, m ↔ feet
  • currency.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.

© 2026 Abhilash Sahoo. Python: Zero to Hero.