How to designate unreachable python code
Question:
What’s the pythonic way to designate unreachable code in python as in:
gender = readFromDB(...) # either 'm' or 'f'
if gender == 'm':
greeting = 'Mr.'
elif gender == 'f':
greeting = 'Ms.'
else:
# What should this line say?
Answers:
raise ValueError('unexpected gender %r' % gender)
This depends on how sure you are of the gender being either 'm'
or 'f'
.
If you’re absolutely certain, use if...else
instead of if...elif...else
. Just makes it easier for everyone.
If there’s any chance of malformed data, however, you should probably raise an exception to make testing and bug-fixing easier. You could use a gender-neutral greeting in this case, but for anything bigger, special values just make bugs harder to find.
You could raise an exception:
raise ValueError("Unexpected gender; expected 'm' or 'f', got %s" % gender)
or use an assert False if you expect the database to return only ‘m’ or ‘f’:
assert False, "Unexpected gender; expected 'm' or 'f', got %s" % gender
I actually think that there’s a place for this.
class SeriousDesignError(Exception):
pass
So you can do this
if number % 2 == 0:
result = "Even"
elif number % 2 == 1:
result = "Odd"
else:
raise SeriousDesignError()
I think this is the most meaningful error message. This kind of thing can only arise through design errors (or bad maintenance, which is the same thing.)
I sometimes do:
if gender == 'm':
greeting = 'Mr.'
else:
assert gender == 'f'
greeting = 'Ms.'
I think this does a good job of telling a reader of the code that there are only (in this case) two possibilities, and what they are. Although you could make a case for raising a more descriptive error than AssertionError.
It depends exactly what you want the error to signal, but I would use a dictionary in this case:
greetings = {'m': 'Mr.', 'f': 'Ms.'}
gender = readFromDB(...) # either 'm' or 'f'
greeting = greetings[gender]
If gender is neither m nor f, this will raise a KeyError containing the unexpected value:
greetings = {'m': 'Mr.', 'f': 'Ms.'}
>>> greetings['W']
Traceback (most recent call last):
File "<pyshell#4>", line 1, in <module>
greetings['W']
KeyError: 'W'
If you want more detail in the message, you can catch & reraise it:
try:
greeting = greetings[gender]
except KeyError,e:
raise ValueError('Unrecognized gender %s' % gender)
Until now, I’ve usually used a variation on John Fouhy’s answer — but this is not exactly correct, as Ethan points out:
assert gender in ('m', 'f')
if gender == 'm':
greeting = 'Mr.'
else:
greeting = 'Ms.'
The main problem with using an assert is that if anyone runs your code with the -O or -OO flags, the asserts get optimized away. As Ethan points out below, that means you now have no data checks at all. Asserts are a development aid and shouldn’t be used for production logic. I’m going to get into the habit of using a check() function instead — this allows for clean calling syntax like an assert:
def check(condition, msg=None):
if not condition:
raise ValueError(msg or '')
check(gender in ('m', 'f'))
if gender == 'm':
greeting = 'Mr.'
else:
greeting = 'Ms.'
Going back to the original question, I’d claim that using an assert() or check() prior to the if/else logic is easier to read, safer, and more explicit:
- it tests the data quality first before starting to act on it — this might be important if there are operators other than ‘==’ in the if/else chain
- it separates the assertion test from the branching logic, rather than interleaving them — this makes reading and refactoring easier
With python version 3.11+, you can use and typing.assert_never
to assert unreachability in a way that can be verified by static type checkers such as mypy
and pyright
. (With pre-3.11 versions of python you’ll need to import assert_never
from the typing_extensions
module.)
Here’s an example using typing.assert_never
together with typing.Literal
to assert that the else
-block from OP’s question is unreachable:
from typing import Literal
from typing_extensions import assert_never
var: Literal["m", "f"] = "m"
if var == "m":
...
elif var == "f":
...
else:
assert_never(var) # unreachable
Static type checkers will verify that the else
-block above is indeed unreachable. If the assert_never
block is reached at runtime, it will throw an AssertionError
:
>>> from typing_extensions import assert_never
>>> assert_never(123)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/homestar/tmp/.direnv/python-3.9.15/lib/python3.9/site-packages/typing_extensions.py", line 1993, in assert_never
raise AssertionError("Expected code to be unreachable")
AssertionError: Expected code to be unreachable
Here is an example with a function whose return-type annotation is typing.NoReturn
:
import time
from typing import NoReturn
from typing_extensions import Never, assert_never
def foo() -> NoReturn:
while True:
time.sleep(1)
bar: Never = foo()
assert_never(bar) # unreachable
The typing.Never
type is available in typing
module for python >=3.11 and in typing_extensions
for earlier versions of python.
Here’s another example using if
/elif
/else
blocks to match against the type of a variable:
from typing import Union
from typing_extensions import assert_never
var: Union[int, str, float] = "123"
if isinstance(var, int):
...
elif isinstance(var, str):
...
elif isinstance(var, float):
...
else:
assert_never(var) # unreachable
For more examples, including matching against the values of an enum type, see the Python typing docs on Unreachable Code and Exhaustiveness Checking.
What’s the pythonic way to designate unreachable code in python as in:
gender = readFromDB(...) # either 'm' or 'f'
if gender == 'm':
greeting = 'Mr.'
elif gender == 'f':
greeting = 'Ms.'
else:
# What should this line say?
raise ValueError('unexpected gender %r' % gender)
This depends on how sure you are of the gender being either 'm'
or 'f'
.
If you’re absolutely certain, use if...else
instead of if...elif...else
. Just makes it easier for everyone.
If there’s any chance of malformed data, however, you should probably raise an exception to make testing and bug-fixing easier. You could use a gender-neutral greeting in this case, but for anything bigger, special values just make bugs harder to find.
You could raise an exception:
raise ValueError("Unexpected gender; expected 'm' or 'f', got %s" % gender)
or use an assert False if you expect the database to return only ‘m’ or ‘f’:
assert False, "Unexpected gender; expected 'm' or 'f', got %s" % gender
I actually think that there’s a place for this.
class SeriousDesignError(Exception):
pass
So you can do this
if number % 2 == 0:
result = "Even"
elif number % 2 == 1:
result = "Odd"
else:
raise SeriousDesignError()
I think this is the most meaningful error message. This kind of thing can only arise through design errors (or bad maintenance, which is the same thing.)
I sometimes do:
if gender == 'm':
greeting = 'Mr.'
else:
assert gender == 'f'
greeting = 'Ms.'
I think this does a good job of telling a reader of the code that there are only (in this case) two possibilities, and what they are. Although you could make a case for raising a more descriptive error than AssertionError.
It depends exactly what you want the error to signal, but I would use a dictionary in this case:
greetings = {'m': 'Mr.', 'f': 'Ms.'}
gender = readFromDB(...) # either 'm' or 'f'
greeting = greetings[gender]
If gender is neither m nor f, this will raise a KeyError containing the unexpected value:
greetings = {'m': 'Mr.', 'f': 'Ms.'}
>>> greetings['W']
Traceback (most recent call last):
File "<pyshell#4>", line 1, in <module>
greetings['W']
KeyError: 'W'
If you want more detail in the message, you can catch & reraise it:
try:
greeting = greetings[gender]
except KeyError,e:
raise ValueError('Unrecognized gender %s' % gender)
Until now, I’ve usually used a variation on John Fouhy’s answer — but this is not exactly correct, as Ethan points out:
assert gender in ('m', 'f')
if gender == 'm':
greeting = 'Mr.'
else:
greeting = 'Ms.'
The main problem with using an assert is that if anyone runs your code with the -O or -OO flags, the asserts get optimized away. As Ethan points out below, that means you now have no data checks at all. Asserts are a development aid and shouldn’t be used for production logic. I’m going to get into the habit of using a check() function instead — this allows for clean calling syntax like an assert:
def check(condition, msg=None):
if not condition:
raise ValueError(msg or '')
check(gender in ('m', 'f'))
if gender == 'm':
greeting = 'Mr.'
else:
greeting = 'Ms.'
Going back to the original question, I’d claim that using an assert() or check() prior to the if/else logic is easier to read, safer, and more explicit:
- it tests the data quality first before starting to act on it — this might be important if there are operators other than ‘==’ in the if/else chain
- it separates the assertion test from the branching logic, rather than interleaving them — this makes reading and refactoring easier
With python version 3.11+, you can use and typing.assert_never
to assert unreachability in a way that can be verified by static type checkers such as mypy
and pyright
. (With pre-3.11 versions of python you’ll need to import assert_never
from the typing_extensions
module.)
Here’s an example using typing.assert_never
together with typing.Literal
to assert that the else
-block from OP’s question is unreachable:
from typing import Literal
from typing_extensions import assert_never
var: Literal["m", "f"] = "m"
if var == "m":
...
elif var == "f":
...
else:
assert_never(var) # unreachable
Static type checkers will verify that the else
-block above is indeed unreachable. If the assert_never
block is reached at runtime, it will throw an AssertionError
:
>>> from typing_extensions import assert_never
>>> assert_never(123)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/homestar/tmp/.direnv/python-3.9.15/lib/python3.9/site-packages/typing_extensions.py", line 1993, in assert_never
raise AssertionError("Expected code to be unreachable")
AssertionError: Expected code to be unreachable
Here is an example with a function whose return-type annotation is typing.NoReturn
:
import time
from typing import NoReturn
from typing_extensions import Never, assert_never
def foo() -> NoReturn:
while True:
time.sleep(1)
bar: Never = foo()
assert_never(bar) # unreachable
The typing.Never
type is available in typing
module for python >=3.11 and in typing_extensions
for earlier versions of python.
Here’s another example using if
/elif
/else
blocks to match against the type of a variable:
from typing import Union
from typing_extensions import assert_never
var: Union[int, str, float] = "123"
if isinstance(var, int):
...
elif isinstance(var, str):
...
elif isinstance(var, float):
...
else:
assert_never(var) # unreachable
For more examples, including matching against the values of an enum type, see the Python typing docs on Unreachable Code and Exhaustiveness Checking.