How to keep multiple IF / ELSE statements manageable

I have been writing software now for over 10 years across a range of different roles and at a range of companies. In that time, I have learned a lot of good practices and a lot of bad practices also (usually through hard-learned experience). This one is a pet hate of mine that I see all too often, even in production code.

output = None
if value == 'A':
    output = 'Something'
elif value == 'B':
    output = 'Something else'
elif value == 'C':
    output = 'Another value'
elif value == 'D':
    output = 'Some other value'
elif value == 'E':
    output = 'Running out of ideas now'
return output

Why this is bad

The fix

The following logic is much cleaner, faster, and more extensible:

output = {
    'A': 'Something',
    'B': 'Something else',
    'C': 'Another value',
    'D': 'Some other value',
    'E': 'Running out of ideas now',
}.get(value)

This can also be used for more than just value-to-value lookup. For example:

value = get_user_input()  # unknown type

converter = {
    datetime: lambda v: v,  # no-op
    int: datetime.fromtimestamp,
    str: datetime.fromisoformat,
}[type(value)]

converted_value = converter(value)

Is it faster?

Code:

import timeit

def if_else_code(value):
    output = None
    if value == 'A':
        output = 'Something'
    elif value == 'B':
        output = 'Something else'
    elif value == 'C':
        output = 'Another value'
    elif value == 'D':
        output = 'Some other value'
    elif value == 'E':
        output = 'Running out of ideas now'
    return output

output_map = {
    'A': 'Something',
    'B': 'Something else',
    'C': 'Another value',
    'D': 'Some other value',
    'E': 'Running out of ideas now',
}

def dict_code(value):
    output = output_map.get(value)
    return output

results = {}

for code in (
    "if_else_code('A')",
    "if_else_code('B')",
    "if_else_code('C')",
    "if_else_code('D')",
    "if_else_code('E')",
    "if_else_code('F')",  # Key does not exist
    "dict_code('A')",
    "dict_code('B')",
    "dict_code('C')",
    "dict_code('D')",
    "dict_code('E')",
    "dict_code('F')",  # Key does not exist
):
    results[code] = timeit.timeit(code, globals=globals(), number=1_000_000)

for code in results:
    print(f"{code:17}: {results[code]:0.6f}")

Results:

{
    "if_else_code('A')": 0.044545,
    "if_else_code('B')": 0.033764,
    "if_else_code('C')": 0.035543,
    "if_else_code('D')": 0.041406,
    "if_else_code('E')": 0.046907,
    "if_else_code('F')": 0.046895,
    "dict_code('A')"   : 0.030236,
    "dict_code('B')"   : 0.030694,
    "dict_code('C')"   : 0.031667,
    "dict_code('D')"   : 0.030722,
    "dict_code('E')"   : 0.031334,
    "dict_code('F')"   : 0.032734,
}

The dict_code version averages 0.033μs per call, even for a key that does not exist. Meanwhile the if_else_code version averages ~0.04μs, but for keys that are specified low-down in the if/else structure or do not exist, we can see that the time taken increases.

What about multiple variables?

In Python, this pattern still holds even when we have multiple variables. For example, instead of:

if value == 'A' and status == 'active':
    output = 'Something'
elif value == 'A' and status == 'inactive':
    output = 'Something else'
if value == 'B' and status == 'active':
    output = 'Another value'
elif value == 'B' and status == 'inactive':
    output = 'Some other value'
else:
    raise ValueError(f'{value}, {status}: Combination not recognised')

or

if value == 'A':
    if status == 'active':
        output = 'Something'
    elif status == 'inactive':
        output = 'Something else'
if value == 'B':
    if status == 'active':
        output = 'Another value'
    elif status == 'inactive':
        output = 'Some other value'
else:
    raise ValueError(f'{value}, {status}: Combination not recognised')

Instead, we can use tuples as keys in our dict:

key = (value, status)
output = {
    ('A', 'active'): 'Something',
    ('A', 'inactive'): 'Something else',
    ('B', 'active'): 'Another value',
    ('B', 'inactive'): 'Some other value',
}[key]

This approach scales well with multiple variables and keeps the code clean and readable.