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?
Asked By: phihag

||

Answers:

raise ValueError('unexpected gender %r' % gender)
Answered By: Dave

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.

Answered By: zenazn

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
Answered By: moinudin

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.)

Answered By: S.Lott

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.

Answered By: John Fouhy

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)
Answered By: Vicki Laidler

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
Answered By: stevegt

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.

Answered By: Jasha
Categories: questions Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.