No history yet

Functional Modular Programming

Crafting Flexible Functions

Writing linear scripts is great for one-off tasks, but real applications demand a more organized approach. Functions are the first step. You already know how to define a function with a fixed set of parameters, but what happens when you don't know how many arguments you'll receive? Python provides a flexible way to handle this.

Imagine you need a function that can sum any number of values. You could require the user to pass a list, but a more direct syntax exists using *args. This syntax gathers any number of positional arguments into a tuple.

# The asterisk before 'numbers' is the key
def sum_all(*numbers):
    print(f"Received: {numbers}") # numbers is a tuple
    total = 0
    for num in numbers:
        total += num
    return total

print(sum_all(1, 2, 3)) # Output: 6
print(sum_all(10, 20, 30, 40)) # Output: 100

Similarly, **kwargs collects an arbitrary number of keyword arguments into a dictionary. This is incredibly useful for functions that configure an object or process optional metadata. The combination of *args and **kwargs allows you to create highly flexible function signatures.

# The double asterisk before 'attributes' is the key
def create_user(username, **attributes):
    user_profile = {'username': username}
    user_profile.update(attributes) # attributes is a dictionary
    return user_profile

user1 = create_user("alex", email="alex@example.com", active=True)
print(user1) 
# Output: {'username': 'alex', 'email': 'alex@example.com', 'active': True}

user2 = create_user("maria")
print(user2)
# Output: {'username': 'maria'}

To improve clarity, Python also supports function annotations. These are optional hints about the expected types of arguments and the return value. They don't enforce types, but they provide valuable documentation for developers and tools like type checkers.

def greet(name: str) -> str:
    return f"Hello, {name}"

Don't Repeat Yourself

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

This is the —Don't Repeat Yourself. When you copy and paste code, you create a maintenance problem. If you find a bug in one place, you must remember to fix it everywhere else you pasted it. This is tedious and error-prone. The solution is to centralize your logic into functions and organize those functions into modules.

A module is simply a Python file (.py). When you import a module, Python executes the file and makes its top-level definitions—like functions and classes—available under a specific name. Each module has its own private symbol table, which acts as the global for all objects defined in that module. This prevents names from colliding between different files.

Let's say we have a file named utils.py with some helper functions.

# utils.py

PI = 3.14159

def calculate_area(radius: float) -> float:
    """Calculates the area of a circle."""
    return PI * (radius ** 2)

def format_price(price: float) -> str:
    """Formats a number as a currency string."""
    return f"💲{price:.2f}"

Now, we can use these functions in another file, like main.py, by importing the utils module.

# main.py

import utils

circle_area = utils.calculate_area(10)
item_price = utils.format_price(49.99)

print(f"The area is: {circle_area}")
print(f"The price is: {item_price}")

Organizing with Packages

As your project grows, you'll accumulate many modules. Throwing them all into one directory becomes messy. The next level of organization is the package. A package is a folder that contains Python modules and a special file called __init__.py.

The __init__.py file tells Python to treat the directory as a package. It can be empty, or it can run initialization code for the package. For example, it can make certain functions from its modules directly accessible.

There are two main ways to import from packages: absolute and relative imports. Absolute imports specify the full path from the project's root folder. They are explicit and clear.

# In main.py

# Absolute import
from store.products import get_product_details
from store.utils.formatting import format_price

product = get_product_details(101)
price = format_price(product['price'])

Relative imports use dots (.) to specify a path from the current module's location. A single dot refers to the current directory, and two dots (..) refer to the parent directory. They are useful for reorganizing packages, but can sometimes be less readable.

# Inside store/products.py, to import from store/utils/formatting.py

# Relative import
from .utils.formatting import format_price

# This means: from the current package, go into the 'utils' sub-package
# and from its 'formatting' module, import 'format_price'.

Best practice favors absolute imports for their clarity. Relative imports are best used for modules inside the same package that are closely related.

Quiz Questions 1/6

What is the primary purpose of *args and **kwargs in a Python function definition?

Quiz Questions 2/6

The DRY principle stands for "Don't Repeat Yourself" and is a fundamental concept in software development aimed at reducing code duplication.

By structuring your code into functions, modules, and packages, you create a foundation for building large, maintainable, and collaborative software projects.