4.8. Modules and Packages
Table of Contents
Why Modules and Packages Matter
As your programs grow, keeping all code in a single file becomes messy and hard to maintain. You need a way to:
- Split code into multiple files.
- Reuse code across projects.
- Organize related functionality together.
This is what modules and packages are for. They are about organizing and reusing code, not about learning a new kind of logic.
In this chapter we focus on the concepts, with examples in a Python-like style, because later you will use this directly in real Python backend projects.
Modules
A module is simply one file that contains Python code.
If you have a file named:
math_utils.py
then math_utils is a module.
Inside that file you might have functions, classes, and variables:
# math_utils.py
PI = 3.14159
def add(a, b):
return a + b
def circle_area(radius):
return PI * radius * radiusYou can then import and use this module from another file:
# main.py
import math_utils
result = math_utils.add(2, 3)
print(result) # 5
print(math_utils.PI) # 3.14159The key ideas:
- One
.pyfile is one module. - The module name is the file name without
.py. - You access things inside the module with
module_name.something.
Why Use Modules?
Imagine a small backend project:
auth.pyfor authentication logic.db.pyfor database access.utils.pyfor helper functions.main.pyas the entry point of the application.
This separation gives you:
- Organization: Each file has a clear responsibility.
- Reusability: You can reuse, for example,
db.pyin another project. - Testability: Easier to write tests for each part.
Example structure:
project/
main.py
auth.py
db.py
utils.py
In main.py you could write:
import auth
import db
def start_app():
db.connect()
auth.setup_auth()Importing Modules
There are several common ways to import modules. Understanding them is crucial because you will see them constantly in backend code.
`import module`
This is the most basic form.
import math_utils
print(math_utils.add(1, 2))- You get the whole module.
- You use the module name as a prefix.
`import module as alias`
Use an alias to shorten long names:
import math_utils as mu
print(mu.add(1, 2))This is common with popular libraries:
import numpy as np
import pandas as pd`from module import name`
If you only need specific items:
from math_utils import add, circle_area
print(add(1, 2))
print(circle_area(5))- You do not use the module prefix.
- Only the imported names are available.
`from module import *` (and why to avoid it)
from math_utils import *This imports everything, which looks convenient but is dangerous in larger projects.
Rule: Avoid from module import * in real projects.
It makes it unclear where names come from and can cause name conflicts.
Python’s Standard Library vs Your Own Modules
You will often import:
- Standard library modules, for example
os,sys,json,datetime. - Third-party modules, installed via
pip, for examplerequests,fastapi. - Your own modules, for example
auth,db,utils.
Example:
import os # standard library
import fastapi # third-party
import utils # your own moduleThe import syntax is the same. Only the source of the module changes.
Packages
A package is a directory that groups related modules.
Minimal Python package structure:
my_app/
__init__.py
auth.py
db.py
utils.pymy_appis a package.auth.py,db.py,utils.pyare modules inside that package.__init__.pytells Python thatmy_appis a package.
In modern Python versions, __init__.py is not always required, but you will see it a lot and it is still common practice, especially in backend projects.
You can import like this:
import my_app.auth
from my_app import db
from my_app.utils import format_dateNested Packages
Packages can contain subpackages, which are packages inside packages.
Example:
shop/
__init__.py
auth/
__init__.py
login.py
register.py
orders/
__init__.py
create.py
list.py
products/
__init__.py
catalog.pyHere:
shopis a top-level package.shop.auth,shop.orders,shop.productsare subpackages.shop.auth.loginis a module inside theauthsubpackage.
You can import like this:
from shop.auth.login import login_user
from shop.orders.create import create_order
import shop.products.catalog as catalogThis style is very typical for backend projects: group related functionality in subpackages.
Absolute vs Relative Imports
In a package, you have two main ways to import:
- Absolute imports
- Relative imports
Absolute Imports
Use the full path from the project root.
Example project:
my_project/
main.py
shop/
__init__.py
auth/
__init__.py
login.py
tokens.py
db/
__init__.py
connection.py
Inside shop/auth/login.py:
# absolute import
from shop.db.connection import get_db
from shop.auth.tokens import create_token- Start from the top-level package
shop. - This is clear and explicit.
Relative Imports
Relative imports use dots to say "from my current package".
Rules:
.means "current package"...means "parent package"....means "parent of parent", and so on.
Inside shop/auth/login.py you can write:
# relative imports
from .tokens import create_token # from the same package 'auth'
from ..db.connection import get_db # from sibling package 'db'Both absolute and relative imports are used in real projects.
Guideline:
Use absolute imports for clarity across the project.
Use relative imports carefully inside packages, usually only when it makes the structure easier to change.
Example: Organizing a Simple Backend
Imagine a tiny backend with:
- User registration and login.
- Database access.
- Some utilities.
Structure:
backend/
__init__.py
main.py
auth/
__init__.py
register.py
login.py
db/
__init__.py
connection.py
models.py
utils/
__init__.py
hashing.py
validators.pySome example module contents:
db/connection.py:
# db/connection.py
def get_db():
print("Connecting to database...")
# return a fake connection for demo
return "db_connection"
auth/register.py:
# auth/register.py
from ..db.connection import get_db
from ..utils.hashing import hash_password
from ..utils.validators import is_valid_email
def register_user(email, password):
if not is_valid_email(email):
raise ValueError("Invalid email")
db = get_db()
password_hash = hash_password(password)
print(f"Saving user {email} with hash {password_hash} to {db}")
utils/hashing.py:
# utils/hashing.py
def hash_password(password: str) -> str:
return f"HASHED::{password}"
utils/validators.py:
# utils/validators.py
def is_valid_email(email: str) -> bool:
return "@" in email and "." in email
main.py:
# main.py
from backend.auth.register import register_user
def main():
register_user("user@example.com", "secret123")
if __name__ == "__main__":
main()This demonstrates:
- Folder structure that matches logical parts of the backend.
- Imports across packages and modules.
- Separation of responsibilities.
Reusing Code with Modules and Packages
Modules and packages allow you to reuse code in multiple places:
- A
hash_passwordfunction inutils/hashing.pycan be used by: - Registration logic.
- Login logic.
- Password reset logic.
Example:
# auth/login.py
from ..db.connection import get_db
from ..utils.hashing import hash_password
def login_user(email, password):
db = get_db()
hashed = hash_password(password)
print(f"Checking user {email} with hash {hashed} in {db}")Instead of copying the hashing logic three times, you put it in one module and import it everywhere.
Important:
Do not copy and paste the same code into multiple files.
Put shared logic into a module, then import it where needed.
This is a core habit for professional backend development.
Avoiding Circular Imports
A circular import happens when:
- Module A imports module B.
- Module B imports module A.
Python loads modules once, in order. If module A tries to import B, and B tries to import A at the same time, you can get errors.
Example of a problem:
auth/login.py:
from auth.session import create_session
def login():
# ...
create_session()
auth/session.py:
from auth.login import login # circular dependency
def create_session():
# ...
login()
Here login.py imports session.py and session.py imports login.py.
This is a circular import.
How to avoid circular imports
- Refactor shared logic into a third module:
auth/
__init__.py
login.py
session.py
helpers.py
Move common functions to helpers.py, then have both login.py and session.py import helpers, instead of importing each other.
- Import inside a function, not at the top (as a last resort):
# session.py
def create_session():
from auth.login import login # local import
# use login here
This delays the import until the function is called, which can work around some circular dependencies.
Prefer refactoring over this when possible, because it is clearer.
`__init__.py` and Package Initialization
The file __init__.py is executed when the package is imported.
For example, in backend/utils/__init__.py:
# backend/utils/__init__.py
print("Initializing backend.utils package")
from .hashing import hash_password
from .validators import is_valid_email
__all__ = ["hash_password", "is_valid_email"]Now when someone does:
from backend import utils- The
printin__init__.pywill execute once. - They can access:
utils.hash_password("secret")Also:
from backend.utils import *
will import only the names listed in __all__. This is one reason __init__.py is still useful.
Naming and Organization Best Practices
For backend projects, clear structure is critical.
File and module naming
- Use lowercase file names:
auth.py, notAuth.py. - Use underscores for multiple words:
user_service.py. - Give descriptive names:
db_connection.pyis better thana1.py.
Package design
Group modules by their purpose:
Typical backend layout:
app/
__init__.py
main.py # application entry point
config.py # configuration
api/ # API route handlers
__init__.py
users.py
auth.py
products.py
services/ # business logic
__init__.py
user_service.py
order_service.py
db/ # database-related code
__init__.py
models.py
connection.py
utils/ # helper functions
__init__.py
hashing.py
email_utils.pyYou will see similar structures in many backend frameworks.
Examples You Can Try Yourself
To practice, create a small project like:
calculator/
main.py
operations/
__init__.py
add.py
subtract.py
multiply.py
divide.py
operations/add.py:
def add(a, b):
return a + b
operations/subtract.py:
def subtract(a, b):
return a - b
main.py:
from operations.add import add
from operations.subtract import subtract
def main():
print(add(5, 3))
print(subtract(5, 3))
if __name__ == "__main__":
main()Then extend this structure, for example:
- Add
operations/advanced/power.py. - Add
operations/advanced/root.py. - Use nested imports like
from operations.advanced.power import power.
This is the same skill you will use later to organize real backend applications.
Summary
- A module is a single
.pyfile. - A package is a directory that contains modules, often with
__init__.py. - Use
import moduleorfrom module import nameto reuse code. - Organize code in packages and subpackages by responsibility, such as
auth,db,utils,api. - Prefer absolute imports for clarity, use relative imports carefully inside packages.
- Avoid circular imports by refactoring shared logic into separate modules.
- Use good naming and structure to keep backend projects understandable and maintainable.
You now have the conceptual tools to structure real-world backend code as your projects grow.
Views: 8
KAHIBARO