As Python projects grow, keeping all code in a single file becomes unmanageable. Modules and packages are Python's solution — they let you split code across multiple files, organise related functionality together, and reuse code across projects. They are also how Python's enormous ecosystem of third-party libraries is delivered and consumed.
What Is a Module?
A module is simply a Python file — any .py file is a module. The filename (without the .py extension) becomes the module name. A module can contain functions, classes, variables, and executable code.
my_project/
├── main.py
├── utils.py ← a module
└── database.py ← a module
Importing a Module
To use code from another module, import it with the import statement:
# utils.py
def greet(name):
return f"Hello, {name}!"
PI = 3.14159
# main.py
import utils
print(utils.greet("Wariz")) # Hello, Wariz!
print(utils.PI) # 3.14159
Importing a module gives you access to its contents via the module name as a namespace — utils.greet, utils.PI. This avoids name collisions if two modules define a function with the same name.
Import Variations
Import specific names — from ... import
from utils import greet, PI
print(greet("Wariz")) # No "utils." prefix needed
print(PI)
This imports specific names directly into the current namespace. Use this when you need only a few things from a module and the names are unambiguous.
Import with an alias — as
import utils as u
print(u.greet("Wariz"))
from utils import greet as say_hello
print(say_hello("Wariz"))
Aliases are commonly used for modules with long names or widely adopted shorthand conventions:
import numpy as np # Convention in data science
import pandas as pd # Convention in data science
import matplotlib.pyplot as plt
Import everything — from module import *
from utils import * # Imports all public names
print(greet("Wariz"))
This approach is generally discouraged — it pollutes the current namespace with unknown names, making it hard to know where a name came from and creating potential conflicts.
The if __name__ == "__main__" Guard
When Python runs a file directly, it sets the special variable __name__ to "__main__". When a file is imported as a module, __name__ is set to the module's name instead.
This means the guard lets you write code that only runs when the script is executed directly — not when it is imported:
# utils.py
def greet(name):
return f"Hello, {name}!"
if __name__ == "__main__":
# This block only runs when utils.py is run directly
# It does NOT run when utils is imported by main.py
print(greet("Test"))
This pattern prevents side effects (like printing output or running tests) from executing when a module is imported by another file.
Python's Standard Library
Python ships with a large standard library — a collection of built-in modules that are always available without installation. A few of the most commonly used:
| Module | What It Provides |
|---|---|
os | Interacting with the operating system — file paths, environment variables, directory operations |
sys | Access to interpreter settings — command-line arguments, Python version, exit codes |
math | Mathematical functions — sqrt, ceil, floor, log, trigonometry |
random | Random number generation and random selection |
datetime | Working with dates, times, and time differences |
json | Encoding and decoding JSON data |
re | Regular expressions for pattern matching in strings |
pathlib | Modern, object-oriented file path handling |
collections | Specialised container types — Counter, defaultdict, deque, namedtuple |
itertools | Tools for working with iterators — combinations, permutations, chaining |
functools | Higher-order function tools — partial, reduce, lru_cache |
typing | Type hints and annotations |
Examples
import math
print(math.sqrt(16)) # 4.0
print(math.pi) # 3.141592653589793
import random
print(random.randint(1, 10)) # Random integer between 1 and 10
print(random.choice(["a", "b", "c"])) # Random item from list
import datetime
today = datetime.date.today()
print(today) # 2024-03-17
from pathlib import Path
p = Path("my_folder") / "data" / "file.txt"
print(p) # my_folder/data/file.txt
What Is a Package?
A package is a directory that contains multiple modules and a special file called __init__.py. The __init__.py file (which can be empty) tells Python that the directory should be treated as a package.
my_project/
├── main.py
└── mypackage/
├── __init__.py ← makes this a package
├── auth.py
├── database.py
└── utils.py
Importing from a package:
import mypackage.auth
from mypackage.utils import greet
from mypackage import database
__init__.py
The __init__.py file runs when the package is imported. It can be empty or it can define what gets exported when someone does from mypackage import *:
# mypackage/__init__.py
from .utils import greet
from .auth import login, logout
# Now users can do:
# from mypackage import greet, login
The . before a module name in __init__.py is a relative import — it refers to modules within the same package.
Relative vs. Absolute Imports
Absolute imports
Reference the full path from the project root:
from mypackage.utils import greet
from mypackage.auth import login
Relative imports
Reference relative to the current module's location using .:
# Inside mypackage/auth.py
from .utils import greet # Same package
from ..other import something # Parent package
.= current package..= parent package
Absolute imports are generally preferred — they are more explicit and work regardless of where the module is executed from. Relative imports are useful within a package to avoid repeating the package name.
Third-Party Packages with pip
Beyond the standard library, Python's ecosystem includes over 500,000 packages on PyPI (the Python Package Index). These are installed with pip:
pip install requests # HTTP library
pip install flask # Web framework
pip install pandas # Data analysis
pip install numpy # Numerical computing
Once installed, they are imported exactly like standard library modules:
import requests
response = requests.get("https://api.github.com")
print(response.status_code) # 200
Managing dependencies with requirements.txt
When sharing a project, list its dependencies in a requirements.txt file so others can install everything with one command:
# Generate from the current environment
pip freeze > requirements.txt
# Install all dependencies
pip install -r requirements.txt
A typical requirements.txt:
flask==3.0.2
requests==2.31.0
python-dotenv==1.0.1
Exploring a Module
To see everything a module provides:
import math
dir(math) # Lists all attributes and functions
help(math) # Full documentation
help(math.sqrt) # Documentation for a specific function
dir() is particularly useful when you are exploring a new library and want to know what tools are available.
Summary
| Concept | What It Is |
|---|---|
| Module | Any .py file — importable by name |
| Package | A directory containing __init__.py and modules |
import module | Imports the module — access via module.name |
from module import name | Imports specific names into current namespace |
import module as alias | Imports with a shorthand name |
__name__ | "__main__" when run directly; module name when imported |
| Standard library | Built-in modules — always available, no installation needed |
| PyPI | Python Package Index — source of third-party packages |
pip install | Installs third-party packages |
requirements.txt | Lists project dependencies for reproducible installs |
| Absolute import | Full path from project root |
| Relative import | .module syntax — relative to current package location |