Modules and Packages

Organizing and Reusing Code

What are Modules?

  • Modules are files containing Python code that can be imported and used
  • They help organize code into logical units
  • Each .py file is a module
  • Modules can contain functions, classes, and variables
  • They promote code reusability and maintainability
# math_utils.py (a module)
def add(a, b):
    return a + b

def multiply(a, b):
    return a * b

PI = 3.14159

Why Use Modules?

Benefits:

  • Code Organization: Group related functionality
  • Reusability: Use code across multiple programs
  • Namespace Management: Avoid naming conflicts
  • Maintainability: Easier to update and debug
  • Collaboration: Team members can work on different modules

Example Structure:

my_project/
├── main.py
├── utils/
│   ├── math_utils.py
│   ├── string_utils.py
│   └── file_utils.py
└── models/
    ├── user.py
    └── product.py

Importing Modules

Import entire module:

import math

result = math.sqrt(16)  # 4.0
print(math.pi)          # 3.141592653589793

Import specific functions:

from math import sqrt, pi

result = sqrt(16)  # No need for math.sqrt
print(pi)          # No need for math.pi

Import with alias:

import math as m
import numpy as np  # Common convention

result = m.sqrt(16)
array = np.array([1, 2, 3])

Different Import Styles

Import everything (use with caution):

from math import *

result = sqrt(16)  # All functions available directly
# Can cause namespace pollution!

Import with custom names:

from math import sqrt as square_root
from math import pi as PI_VALUE

result = square_root(16)
print(PI_VALUE)

Conditional imports:

try:
    import numpy as np
    HAS_NUMPY = True
except ImportError:
    HAS_NUMPY = False
    print("NumPy not available")

Built-in Modules

Common built-in modules:

import os        # Operating system interface
import sys       # System-specific parameters
import datetime  # Date and time handling
import random    # Random number generation
import json      # JSON encoder/decoder
import re        # Regular expressions
import collections  # Specialized container datatypes

Examples:

import os
print(os.getcwd())  # Current working directory

import datetime
now = datetime.datetime.now()
print(now.strftime("%Y-%m-%d %H:%M:%S"))

import random
number = random.randint(1, 100)

Creating Your Own Modules

Create a module file: calculator.py

"""A simple calculator module."""

def add(a, b):
    """Add two numbers."""
    return a + b

def subtract(a, b):
    """Subtract second number from first."""
    return a - b

def multiply(a, b):
    """Multiply two numbers."""
    return a * b

def divide(a, b):
    """Divide first number by second."""
    if b != 0:
        return a / b
    else:
        raise ValueError("Cannot divide by zero!")

# Module-level variable
VERSION = "1.0.0"

Using Your Custom Module

In another file: main.py

import calculator

# Use the functions
result1 = calculator.add(10, 5)
result2 = calculator.subtract(10, 5)

print(f"Addition: {result1}")
print(f"Subtraction: {result2}")
print(f"Calculator version: {calculator.VERSION}")

Or import specific functions:

from calculator import add, multiply, VERSION

result = add(10, 5)
product = multiply(3, 4)
print(f"Version: {VERSION}")

Module Search Path

Python looks for modules in this order:

import sys
print(sys.path)
  1. Current directory
  2. PYTHONPATH environment variable directories
  3. Standard library directories
  4. Site-packages directory (third-party packages)

Adding custom paths:

import sys
sys.path.append('/path/to/my/modules')

# Now Python can find modules in that directory
import my_custom_module

Packages

Packages are directories containing multiple modules:

my_package/
├── __init__.py      # Makes it a package
├── module1.py
├── module2.py
└── subpackage/
    ├── __init__.py
    └── module3.py

The __init__.py file: - Makes a directory a Python package - Can be empty or contain initialization code - Controls what gets imported with from package import *

Creating a Package

Directory structure:

math_tools/
├── __init__.py
├── basic.py
└── advanced.py

math_tools/__init__.py:

"""Math tools package."""
from .basic import add, subtract
from .advanced import fibonacci, factorial

__version__ = "1.0.0"
__all__ = ['add', 'subtract', 'fibonacci', 'factorial']

math_tools/basic.py:

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

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

Using Packages

Import from package:

# Import entire package
import math_tools
result = math_tools.add(5, 3)

# Import specific modules
from math_tools import basic
result = basic.add(5, 3)

# Import specific functions
from math_tools.basic import add
result = add(5, 3)

# Import from package root (if defined in __init__.py)
from math_tools import add
result = add(5, 3)

Relative vs Absolute Imports

Absolute imports (from project root):

from math_tools.basic import add
from math_tools.advanced import fibonacci

Relative imports (within package):

# In math_tools/advanced.py
from .basic import add        # Same package
from ..other_package import something  # Parent package

# In math_tools/__init__.py
from .basic import add, subtract
from .advanced import fibonacci

The __name__ Variable

Every module has a __name__ attribute:

# In calculator.py
print(f"Module name: {__name__}")

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

# This runs only when file is executed directly
if __name__ == "__main__":
    print("Calculator module is being run directly")
    print(f"2 + 3 = {add(2, 3)}")

When imported: __name__ is the module name When run directly: __name__ is "__main__"

Module Documentation

Good module documentation:

"""
utils.py - Utility functions for data processing

This module provides common utility functions for:
- String manipulation
- File operations
- Data validation

Example:
    >>> from utils import clean_string
    >>> clean_string("  Hello World  ")
    'Hello World'
"""

def clean_string(text):
    """
    Remove leading/trailing whitespace and normalize.
    
    Args:
        text (str): The string to clean
        
    Returns:
        str: Cleaned string
    """
    return text.strip()

Third-Party Packages

Installing packages with pip:

pip install requests
pip install numpy
pip install pandas

Using third-party packages:

import requests
import numpy as np
import pandas as pd

# Make HTTP requests
response = requests.get('https://api.github.com')

# Create arrays
array = np.array([1, 2, 3, 4, 5])

# Work with data
df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})

Virtual Environments

Why use virtual environments? - Isolate project dependencies - Avoid version conflicts - Keep global Python clean

Creating and using virtual environments:

# Create virtual environment
python -m venv myenv

# Activate (Windows)
myenv\Scripts\activate

# Activate (macOS/Linux)
source myenv/bin/activate

# Install packages
pip install requests numpy

# Deactivate
deactivate

Package Management

Requirements file:

# Create requirements.txt
pip freeze > requirements.txt

# Install from requirements
pip install -r requirements.txt

Example requirements.txt:

requests==2.28.1
numpy==1.21.0
pandas>=1.3.0
matplotlib~=3.5.0

Practical Example: File Utilities Package

Package structure:

file_utils/
├── __init__.py
├── readers.py
└── writers.py

file_utils/readers.py:

def read_text_file(filename):
    """Read entire text file."""
    with open(filename, 'r', encoding='utf-8') as file:
        return file.read()

def read_lines(filename):
    """Read file lines as list."""
    with open(filename, 'r', encoding='utf-8') as file:
        return [line.strip() for line in file]

Exercise Time! 🚀

Challenge 1: Create a String Utils Module

# Create string_utils.py with these functions:

def reverse_string(text):
    # Return reversed string
    pass

def count_words(text):
    # Return word count
    pass

def title_case(text):
    # Convert to title case
    pass

Challenge 2: Create a Simple Package

helpers/
├── __init__.py
├── math_helpers.py
└── string_helpers.py

Exercise Solutions

String Utils Module:

def reverse_string(text):
    return text[::-1]

def count_words(text):
    return len(text.split())

def title_case(text):
    return ' '.join(word.capitalize() for word in text.split())

Package __init__.py:

from .math_helpers import add, multiply
from .string_helpers import reverse_string, count_words

__version__ = "1.0.0"

Best Practices

  • Use descriptive module names: database_utils not db_stuff
  • Keep modules focused: One clear purpose per module
  • Document your modules: Include docstrings and examples
  • Use __init__.py: Control package imports
  • Avoid circular imports: Design clear dependency hierarchies
  • Use virtual environments: Isolate project dependencies
  • Pin package versions: Use specific versions in production

Common Pitfalls

1. Circular imports:

# module_a.py
from module_b import function_b

# module_b.py
from module_a import function_a  # Circular import!

2. Modifying sys.path incorrectly:

# Bad
sys.path.insert(0, '/some/absolute/path')

# Better
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'modules'))

3. Using from module import *:

# Avoid this - can cause namespace pollution
from math import *
from numpy import *  # May override math functions!

Module Reloading

For development/debugging:

import importlib
import my_module

# Reload module after changes
importlib.reload(my_module)

Note: Reloading has limitations and should mainly be used during development

Summary

  • Modules organize code into reusable files
  • Packages group related modules together
  • Python has many built-in modules for common tasks
  • Use import statements to access module functionality
  • Create virtual environments for project isolation
  • Follow best practices for clean, maintainable code organization

Next Steps

Coming up next: - Object-oriented programming concepts - Classes and inheritance - Advanced Python features