Worked error handling example#

This notebook shows very thorough error handling for the functions

  • create_markdown_table

  • convert_lod_to_dol

  • convert_dol_to_lod

Such a level of error handling would be necessary if these functions are used by a large group of users (e.g. if they are in a Python package). Whether you need this level of error handling for functions that are only used by yourself in a single project is debatable. But usually thorough error handling saves more time than it costs.

def convert_lod_to_dol(lod):
    """Convert a list of dictionaries to a dictionary of lists.

    Args:
        lod (list): List of dictionaries

    Returns:
        dict: Dictionary of lists

    """
    _fail_if_not_list(lod)
    _fail_if_list_of_wrong_types(lod)
    _fail_if_list_of_dicts_with_different_keys(lod)

    keys = list(lod[0])
    out = {}
    for key in keys:
        out[key] = [d[key] for d in lod]
    return out


def convert_dol_to_lod(dol):
    """Convert a dictionary of lists to a list of dictionaries.

    Args:
        dol (dict): Dictionary of lists

    Returns:
        list: List of dictionaries

    """
    _fail_if_not_dict(dol)
    _fail_if_dict_of_wrong_types(dol)
    _fail_if_dict_of_lists_with_different_lengths(dol)

    keys = list(dol)
    n_rows = len(dol[keys[0]])
    out = []
    for row in range(n_rows):
        out.append({key: dol[key][row] for key in keys})
    return out


def create_markdown_table(data):
    """Create a markdown table from a list of dictionaries or a dictionary of lists.

    Args:
        data (list or dict): List of dictionaries or dictionary of lists

    Returns:
        str: The Markdown table

    """
    _fail_if_neither_dict_nor_list(data)
    if isinstance(data, dict):
        lod = convert_dol_to_lod(data)
    else:
        _fail_if_list_of_wrong_types(data)
        _fail_if_list_of_dicts_with_different_keys(data)
        lod = data

    keys = list(lod[0])

    lines = [
        _create_header(keys),
        _create_separator(len(keys)),
    ]

    for row in lod:
        lines.append(_create_data_row(row))

    return "\n".join(lines)


def _create_header(keys):
    """Create a header for a Markdown table."""
    header = "|"
    for key in keys:
        header += f" {key} |"
    return header


def _create_separator(n_cols):
    separator = "|"
    for _ in range(n_cols):
        separator += " ------ |"
    return separator


def _create_data_row(row_dict):
    """Create a row of data for a Markdown table."""
    row_string = "|"
    for key in row_dict:
        row_string += f" {row_dict[key]} |"
    return row_string


def _fail_if_neither_dict_nor_list(data):
    if not isinstance(data, list | dict):
        msg = f"data must be a list of dicts or a dictionary of lists. Not {type(data)}"
        raise TypeError(
            msg,
        )


def _fail_if_not_list(data):
    if not isinstance(data, list):
        raise TypeError("data must be a list of dicts")


def _fail_if_not_dict(data):
    if not isinstance(data, dict):
        raise TypeError("data must be a dictionary of lists")


class NonTabularDataError(Exception):
    """Raised when data has the correct type but is not tabular."""


def _fail_if_list_of_wrong_types(data):
    invalid_rows = []
    for i, row in enumerate(data):
        if not isinstance(row, dict):
            invalid_rows.append(i)

    if invalid_rows:
        report = "The following rows are not dictionaries:\n"
        for i in invalid_rows:
            report += f"  Row {i} has type {type(data[i])}\n"
        raise TypeError(report)


def _fail_if_list_of_dicts_with_different_keys(data):
    keys = set(data[0].keys())
    invalid_rows = []
    for i, row in enumerate(data):
        if set(row.keys()) != keys:
            invalid_rows.append(i)

    if invalid_rows:
        report = f"Valid keys are: {keys}\n\nThe following rows have invalid keys:\n"

        for i in invalid_rows:
            report += f"  Row {i}: {[k for k in list(data[i]) if k not in keys]}\n"
        raise NonTabularDataError(report)


def _fail_if_dict_of_wrong_types(data):
    invalid_cols = []
    for key in data:
        if not isinstance(data[key], list):
            invalid_cols.append(key)

    if invalid_cols:
        report = "The following dict entries are not lists:\n"
        for key in invalid_cols:
            report += f"  Key '{key}' has a value of type {type(data[key])}\n"
        raise TypeError(report)


def _fail_if_dict_of_lists_with_different_lengths(data):
    length = len(data[next(iter(data.keys()))])
    invalid_cols = []
    for key in data:
        if len(data[key]) != length:
            invalid_cols.append(key)

    if invalid_cols:
        report = (
            f"The correct length is {length}. The following dict entries have invalid "
            "lengths:\n"
        )
        for key in invalid_cols:
            report += f"  Key '{key}' has a value with length {len(data[key])}\n"
        raise NonTabularDataError(report)

Recipe for good error handling#

  1. Identify which inputs can potentially cause probles. Those are the inputs that:

    • come directly from a user

    • have not been checked in other functions

  2. List everything that could go wrong with those inputs. Start with those that are very easy to check (e.g. is data a list or dict) and continue with more specific ones (does the dict have the right keys).

  3. Write one _fail ... function for each of the conditions in the previous step

  4. Call the _fail ... functions at the earliest possible moment in your code

  5. Call your functions with invalid inputs and read the error messages. Are they as helpful as they can be? Do they show too much or too little output? Try to maximize the information content without showing anything irrelevant.

Calling the functions with invalid inputs#

Call create_markdown_table with the inputs below to see the error messages

invalid_lod = [
    {"name", "ProgrammingGod42"},
    {"name": "Kim", "github_name": "CodingKim"},
    ["Jesse", "JavascriptJesse"],
]
invalid_lod = [
    {"name": "Robin", "github_name": "ProgrammingGod42"},
    {"name": "Kim", "github_name": "CodingKim"},
    {"name": "Jesse", "github_nameeeeeeeeee": "JavascriptJesse"},
]
invalid_dol = {
    "name": ("Robin", "Kim", "Jesse"),
    "github_name": ["ProgrammingGod42", "CodingKim", "JavascriptJesse"],
}
invalid_dol = {
    "name": ["Robin", "Kim", "Jesse"],
    "github_name": ["ProgrammingGod42", "CodingKim"],
}