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#
Identify which inputs can potentially cause probles. Those are the inputs that:
come directly from a user
have not been checked in other functions
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).
Write one
_fail ...
function for each of the conditions in the previous stepCall the
_fail ...
functions at the earliest possible moment in your codeCall 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"],
}