Learning Objectives
By the end of this tutorial, you will be able to:
- Understand what modules and packages are and why they matter
- Use
import,from...import, andasaliases effectively - Navigate Python's module search path and
sys.path - Create packages with
__init__.pyfiles - Choose between relative and absolute imports
- Understand namespace packages and PEP 420
- Avoid circular imports and other common pitfalls
What Are Modules?
A module is any .py file. Modules let you break code into reusable, logically grouped pieces.
# math_utils.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
PI = 3.14159
# main.py
import math_utils
print(math_utils.add(5, 3)) # 8
print(math_utils.subtract(10, 4)) # 6
print(math_utils.PI) # 3.14159
Python ships with a standard library of built-in modules:
import os, sys, json
print(os.getcwd())
print(sys.version)
print(json.dumps({"key": "value"}))
Import Statements
Basic import
import math
print(math.sqrt(16)) # 4.0
from...import
Pull specific names directly into your namespace:
from math import sqrt, pi
print(sqrt(25)) # 5.0
import...as Aliases
Give modules or functions shorter names:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from math import sqrt as square_root
print(square_root(49)) # 7.0
Wildcard Imports (Use Sparingly)
from math import *
print(sqrt(100)) # 10.0
Best Practice: Avoid
from module import *. It pollutes your namespace and makes it unclear where names originate.
The Import System
When you run import math_utils, Python:
- Checks if the module is already in
sys.modules - Searches
sys.pathfor a matching file - Compiles and executes the module code
- Stores it in
sys.modules - Binds the name to the current namespace
sys.path
import sys
for path in sys.path:
print(path)
/home/user/projects/myapp
/usr/lib/python3.11
/usr/lib/python3.11/site-packages
import sys
sys.path.append("/home/user/my_modules")
import my_custom_module
__name__
Every module has a special __name__ variable. When run directly, it equals "__main__":
# calculator.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
if __name__ == "__main__":
print(add(2, 3)) # 5
print(subtract(10, 4)) # 6
This pattern lets modules work both as importable libraries and standalone scripts.
Packages
A package is a directory containing Python modules and an __init__.py file.
__init__.py
The __init__.py file marks a directory as a Python package:
my_package/
__init__.py
module_a.py
module_b.py
# my_package/__init__.py
from .module_a import some_function
# my_package/module_a.py
def some_function():
print("Function from module_a")
# main.py
from my_package import module_a
module_a.some_function() # Function from module_a
Flat vs Regular Packages
Flat Package:
my_package/
__init__.py
a.py
b.py
Regular (Nested) Package:
my_project/
__init__.py
models/
__init__.py
user.py
utils/
__init__.py
validators.py
# my_project/models/user.py
class User:
def __init__(self, name, email):
self.name = name
self.email = email
# my_project/utils/validators.py
def validate_email(email):
return "@" in email
# main.py
from my_project.models.user import User
from my_project.utils.validators import validate_email
user = User("Alice", "alice@example.com")
print(validate_email(user.email)) # True
Sub-packages
# my_project/models/__init__.py
from .user import User
from .product import Product
# main.py
from my_project.models import User
user = User("Bob")
print(user.name) # Bob
Relative vs Absolute Imports
Absolute Imports
Absolute imports specify the full path from the project root:
# my_project/services/auth.py
from my_project.models.user import User
from my_project.utils.validators import validate_email
class AuthService:
def login(self, username, email):
return validate_email(email)
Relative Imports
Relative imports use dots to specify position relative to the current file:
# my_project/services/auth.py
from ..models.user import User
from ..utils.validators import validate_email
One dot (.) means the current directory. Two dots (..) means the parent directory.
# my_project/models/user.py
from . import helpers # Same directory
# my_project/services/payment.py
from ..models.product import Product # Parent dir, then into models
| Scenario | Recommendation |
|---|---|
| Same package | Use relative imports |
| Different top-level package | Use absolute imports |
| Deeply nested modules | Use relative for clarity |
| Public libraries | Use absolute for readability |
Namespace Packages (PEP 420)
Namespace packages let you split a package across multiple directories. They do not require __init__.py.
my_package/ # Traditional - needs __init__.py
__init__.py
module_a.py
my_namespace/ # Namespace - no __init__.py needed
module_a.py
Splitting Across Directories
# /path/to/project_a/my_namespace/utils.py
def helper():
return "Helper from project A"
# /path/to/project_b/my_namespace/models.py
class Model:
pass
# main.py
import sys
sys.path.extend(["/path/to/project_a", "/path/to/project_b"])
from my_namespace.utils import helper
from my_namespace.models import Model
print(helper()) # Helper from project A
Module Search Path
Python searches for modules in order: the current directory, directories in PYTHONPATH, and default installation-dependent directories.
import sys
for i, path in enumerate(sys.path):
print(f"{i}: {path}")
Third-party packages from pip install into site-packages.
The __all__ Variable
__all__ controls what gets exported when using from module import *.
# module_with_all.py
__all__ = ["public_func", "PublicClass"]
def public_func():
return "I'm public"
def _private_func():
return "I'm private"
class PublicClass:
pass
from module_with_all import *
print(public_func()) # I'm public
# _private_func() # NameError
Without __all__, from module import * imports everything not starting with _.
Circular Imports
A circular import happens when two modules try to import each other, causing an ImportError.
# module_a.py | # module_b.py
import module_b | import module_a
Solutions:
- Extract shared code to a third module
- Use lazy imports inside functions:
def func(): from module_b import func_b
Common Patterns
Re-export via __init__.py
# my_package/__init__.py
from .module_a import ClassA
from .module_b import function_b
Entry Point Pattern
def main():
print("Application starting...")
if __name__ == "__main__":
main()
Private Module Convention
Modules starting with _ are internal: _internal.py is not part of the public API.
Common Mistakes
- Forgetting
importexecutes code: Top-level code in a module runs on every import. Guard side effects withif __name__ == "__main__". - Wildcard imports:
from module import *pollutes your namespace. Use explicit imports instead. - Shadowing module names:
math = "hello"shadows themathmodule. Use aliases likeimport math as m. - Forgetting
__init__.py: Always include__init__.pyfor traditional packages. - Circular imports: Extract shared code to a third module, or use lazy imports inside functions.
Practice Exercises
Exercise 1: Create a Calculator Package
Create a calculator/ package with basic.py and advanced.py modules.
Solution:
# calculator/basic.py
def add(a, b): return a + b
def subtract(a, b): return a - b
def multiply(a, b): return a * b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# calculator/advanced.py
import math
def power(base, exponent): return base ** exponent
def square_root(n): return math.sqrt(n)
def factorial(n): return math.factorial(n)
# calculator/__init__.py
from .basic import add, subtract, multiply, divide
from .advanced import power, square_root, factorial
# main.py
from calculator import add, multiply, square_root, factorial
print(add(5, 3)) # 8
print(multiply(4, 7)) # 28
print(square_root(16)) # 4.0
print(factorial(5)) # 120
Exercise 2: Build a Logging Utility
Create a package with a ConsoleLogger and FileLogger. Structure:
logger_utils/
__init__.py
console_logger.py
file_logger.py
Solution:
# logger_utils/console_logger.py
import datetime
class ConsoleLogger:
def __init__(self, prefix="LOG"):
self.prefix = prefix
def info(self, message):
ts = datetime.datetime.now().strftime("%H:%M:%S")
print(f"[{self.prefix}] [{ts}] INFO: {message}")
def error(self, message):
ts = datetime.datetime.now().strftime("%H:%M:%S")
print(f"[{self.prefix}] [{ts}] ERROR: {message}")
# logger_utils/file_logger.py
import datetime
class FileLogger:
def __init__(self, filename):
self.filename = filename
def log(self, level, message):
ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(self.filename, "a") as f:
f.write(f"[{ts}] {level}: {message}\n")
# logger_utils/__init__.py
from .console_logger import ConsoleLogger
from .file_logger import FileLogger
# main.py
from logger_utils import ConsoleLogger, FileLogger
console = ConsoleLogger("APP")
console.info("Application started")
file_logger = FileLogger("app.log")
file_logger.log("INFO", "User logged in")
Exercise 3: Resolve a Circular Import
Fix this circular import between order.py and customer.py:
# order.py (BROKEN)
from customer import get_customer
def create_order(customer_id):
customer = get_customer(customer_id)
return {"order_id": 1, "customer": customer}
# customer.py (BROKEN)
from order import create_order
def get_customer(customer_id):
return {"id": customer_id, "name": "Alice"}
def place_order(customer_id):
return create_order(customer_id)
Solution: Move imports inside functions:
# order.py (FIXED)
def create_order(customer_id):
from customer import get_customer
return {"order_id": 1, "customer": get_customer(customer_id)}
# customer.py (FIXED)
def get_customer(customer_id):
return {"id": customer_id, "name": "Alice"}
def place_order(customer_id):
from order import create_order
return create_order(customer_id)
Key Takeaways
| Concept | Summary |
|---|---|
| Module | Any .py file |
| Package | Directory with __init__.py |
import module | Imports the entire module |
from module import name | Imports a specific name |
import module as alias | Renames the module locally |
__name__ == "__main__" | Guards code that runs only when executed directly |
sys.path | List of directories Python searches for modules |
| Relative imports | Use dots (. for current, .. for parent) |
| Namespace packages | Package without __init__.py, split across directories |
__all__ | Controls from module import * behavior |
| Circular imports | Avoid; use lazy imports or restructure code |
Modules and packages are the foundation of well-organized Python code. Mastering them lets you build maintainable, scalable applications that grow without becoming unwieldy.