Functions, execution flow, exceptions and samurais

Categories

Functions, execution flow, exceptions and samurais

What does properly written code has to do with the samurai?

The samurai were not just warriors; they were adherents to a code that emphasized honor, clarity, and decisive action. Similarly, a well-designed function doesn’t just perform tasks — it also knows how to fail properly. When a function encounters a state where it can’t achieve its intended outcome due to invalid inputs or other constraints, it shouldn’t just fail silently or, worse, return an incorrect result. Instead, it should throw an exception and clearly communicate the failure. This is akin to a samurai choosing to die honorably when they cannot fulfill their duty due to circumstances beyond their control.

In practical terms, this means that our functions should not obscure their failures. They should illuminate them with clear, specific exceptions. This approach doesn’t just prevent the program from crashing; it ensures that when issues arise, they do so with clear feedback that aids quick resolution. It’s about making our code robust and maintainable, understanding that like a samurai, every line of code should serve with honor and clarity.

Let’s illustrate this in code

Imagine a function that reads settings from multiple sources and aggregates them into a single settings object.

import os
import json

def read_config_file(path):
    try:
        with open(path, 'r') as file:
            return json.load(file)
    except FileNotFoundError:
        return {}

def read_environment_variables():
    return {
        'host': os.getenv('DB_HOST'),
        'port': os.getenv('DB_PORT')
    }

def read_database_settings():
    # Simulate a scenario where this fails to fetch data
    return {}

def aggregate_settings():
    config = read_config_file('config.json')
    env_vars = read_environment_variables()
    db_settings = read_database_settings()
    # Aggregates all settings into a single dictionary
    settings = {**config, **env_vars, **db_settings}
    return settings

# Main routine
if __name__ == "__main__":
    # do stuff ....
    # .....
    settings = aggregate_settings()
    data_base_user_name = settings["user_name"] # !!! key error here

Settings initialization appears to have finished “successfully,” and we’re ready to authorize a user. But wait… there’s a KeyError. Why? At first glance, this issue might seem straightforward — it’s because the configuration file was missing, as read_config_file didn’t find it. Or wait… did the error occur during read_database_settings? Or perhaps the file was there, but the process lacked the necessary read permissions? Such confusion underscores a classic problem in software development: poor error handling where failures are silent and deferred, leading to diagnostic challenges.

This code exemplifies the necessity of addressing errors as they occur, rather than allowing them to propagate undetected. By adopting the samurai principle, we commit to ensuring that any failure in configuration loading is immediately recognized and handled appropriately, thereby improving the robustness and reliability of our systems.

Let’s enhance this example to better align with the principle:

import os
import json

def read_config_file(path):
    try:
        with open(path, 'r') as file:
            return json.load(file)
    except FileNotFoundError:
        raise FileNotFoundError(f"Configuration file not found: {path}")

def read_environment_variables():
    host = os.getenv('DB_HOST')
    port = os.getenv('DB_PORT')
    if not host or not port:
        raise EnvironmentError("DB_HOST and/or DB_PORT not set in environment variables.")
    return {
        'host': host,
        'port': port
    }

def read_database_settings():
    config_service_url = "http://config-service.local/db-settings"

    try:
        response = requests.get(config_service_url)
        response.raise_for_status()  # Raises an HTTPError for bad responses (4xx or 5xx)
        db_settings = response.json()
        
        # Ensure all required settings are present
        required_keys = ['user', 'password', 'host', 'port']
        if not all(key in db_settings for key in required_keys):
            missing_keys = [key for key in required_keys if key not in db_settings]
            raise ValueError(f"Missing database settings for keys: {', '.join(missing_keys)}")

        return db_settings

    except requests.RequestException as e:
        # Handle any requests-related errors (e.g., network issues, invalid response)
        raise ConnectionError(f"Failed to fetch database settings from {config_service_url}: {e}")

    except ValueError as e:
        # Handle missing keys in the settings
        raise ConnectionError(f"Invalid database settings: {e}")

def aggregate_settings():
    config = read_config_file('config.json')
    env_vars = read_environment_variables()
    db_settings = read_database_settings()

    # Merge all settings into one dictionary
    settings = {**config, **env_vars, **db_settings}
    return settings

# Main routine
if __name__ == "__main__":
    # do stuff ....
    # .....
    settings = aggregate_settings() # !!! exception here
    data_base_user_name = settings["user_name"]

This code clearly indicates that the database username originates from a specific service, which will greatly assist any developer reviewing this code months later. Should there be an issue with the service or if the data received is insufficient, execution will be halted, and a clear exception will be thrown. Consider the section handling file reading: although the programmer addressed only the ‘FileNotFound’ exception explicitly, the design ensures that no exceptions from json.load are suppressed. This approach significantly simplifies troubleshooting should any issues arise.

Till next time, beautiful coders!

Written by: Sviatoslav Siurin

More to explorer