KAHIBARO
Discord Login Register

4.8. Modules and Packages

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:

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:

text
math_utils.py

then math_utils is a module.

Inside that file you might have functions, classes, and variables:

python
# math_utils.py
PI = 3.14159
def add(a, b):
    return a + b
def circle_area(radius):
    return PI * radius * radius

You can then import and use this module from another file:

python
# main.py
import math_utils
result = math_utils.add(2, 3)
print(result)          # 5
print(math_utils.PI)   # 3.14159

The key ideas:

Why Use Modules?

Imagine a small backend project:

This separation gives you:

Example structure:

text
project/
  main.py
  auth.py
  db.py
  utils.py

In main.py you could write:

python
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.

python
import math_utils
print(math_utils.add(1, 2))

`import module as alias`

Use an alias to shorten long names:

python
import math_utils as mu
print(mu.add(1, 2))

This is common with popular libraries:

python
import numpy as np
import pandas as pd

`from module import name`

If you only need specific items:

python
from math_utils import add, circle_area
print(add(1, 2))
print(circle_area(5))

`from module import *` (and why to avoid it)

python
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:

Example:

python
import os       # standard library
import fastapi  # third-party
import utils    # your own module

The 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:

text
my_app/
  __init__.py
  auth.py
  db.py
  utils.py

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:

python
import my_app.auth
from my_app import db
from my_app.utils import format_date

Nested Packages

Packages can contain subpackages, which are packages inside packages.

Example:

text
shop/
  __init__.py
  auth/
    __init__.py
    login.py
    register.py
  orders/
    __init__.py
    create.py
    list.py
  products/
    __init__.py
    catalog.py

Here:

You can import like this:

python
from shop.auth.login import login_user
from shop.orders.create import create_order
import shop.products.catalog as catalog

This 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

Use the full path from the project root.

Example project:

text
my_project/
  main.py
  shop/
    __init__.py
    auth/
      __init__.py
      login.py
      tokens.py
    db/
      __init__.py
      connection.py

Inside shop/auth/login.py:

python
# absolute import
from shop.db.connection import get_db
from shop.auth.tokens import create_token

Relative Imports

Relative imports use dots to say "from my current package".

Rules:

Inside shop/auth/login.py you can write:

python
# 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:

Structure:

text
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.py

Some example module contents:

db/connection.py:

python
# db/connection.py
def get_db():
    print("Connecting to database...")
    # return a fake connection for demo
    return "db_connection"

auth/register.py:

python
# 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:

python
# utils/hashing.py
def hash_password(password: str) -> str:
    return f"HASHED::{password}"

utils/validators.py:

python
# utils/validators.py
def is_valid_email(email: str) -> bool:
    return "@" in email and "." in email

main.py:

python
# main.py
from backend.auth.register import register_user
def main():
    register_user("user@example.com", "secret123")
if __name__ == "__main__":
    main()

This demonstrates:

Reusing Code with Modules and Packages

Modules and packages allow you to reuse code in multiple places:

Example:

python
# 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:

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:

python
from auth.session import create_session
def login():
    # ...
    create_session()

auth/session.py:

python
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

  1. Refactor shared logic into a third module:
text
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.

  1. Import inside a function, not at the top (as a last resort):
python
# 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:

python
# 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:

python
from backend import utils
python
utils.hash_password("secret")

Also:

python
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

Package design

Group modules by their purpose:

Typical backend layout:

text
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.py

You will see similar structures in many backend frameworks.


Examples You Can Try Yourself

To practice, create a small project like:

text
calculator/
  main.py
  operations/
    __init__.py
    add.py
    subtract.py
    multiply.py
    divide.py

operations/add.py:

python
def add(a, b):
    return a + b

operations/subtract.py:

python
def subtract(a, b):
    return a - b

main.py:

python
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:

This is the same skill you will use later to organize real backend applications.


Summary

You now have the conceptual tools to structure real-world backend code as your projects grow.

Views: 8

Comments

Please login to add a comment.

Don't have an account? Register now!