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
It's verbose - We have to duplicate an entire block which contains the logic check as well as the output code to run. Each of these steps allows bugs to be introduced (likely copy/paste errors).
It's difficult to read, and readability matters - code is read ~10 times more than it is written. When you're debugging this code at 2am after a production outage, you need to understand it quickly and with minimal mental overhead.
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)
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.
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.