It is ok to consider that lists used as logical values will not be converted to True/False in Python?

Viewed 61

I am studying a program by Peter Norvig, of AI book fame, found here and he uses this construction:

def candidates(word): 
    "Generate possible spelling corrections for word."
    return (known([word]) or known(edits1(word)) or known(edits2(word)) or [word])

I do understand how it works. For the sake of other readers, I will explain here.

Functions known, edits1 and edits2 return lists that can be empty.

The code uses the truth value of these lists to find the first that evaluates to True. Since Python short circuits the expression, that will be the answer.

But, for this expression to work, it is necessary that the operands are used as the return value, i.e. the lists will be kept as they are received, and not transformed to True or False while the logical expression is interpreted.

Another way to do that, using walrus, would be:

  if a:=known([word]):
    return a
  elif b:=know(edits1(word)): #fixed elsif type on edit
    return b
  elif c:=know(edits2(word)): #fixed elsif type on edit
    return c
  else: 
    return [word]

I have two questions:

  1. Is the assumption that the value returned will be the original value of the operand, and not its truth value, the expected and correct behavior for any Python interpreter?
  2. Is this the Pythonic style?
2 Answers

To expand on my comment, python allows you to operate on the idea of 'truthiness' of a container. For built-in containers, this defaults to whether or not they are empty. So [] (an empty list) is falsey, but it is not False. You can test these things rather directly:

Python 3.8.0 (v3.8.0:fa919fdf25, Oct 14 2019, 10:23:27) 
>>> if []: print('Nope!')
... 
>>> if [] == False: print('Nope!')
... 
>>> if [] is False: print('Nope!')
... 
>>> if ['a']: print('Yep!')
... 
Yep!
>>> 

"Is the assumption that the value returned will be the original value of the operand, and not its truth value, the expected and correct behavior for any Python interpreter?"

Yes, the returned value will be the "original value". That is, a list is returned. An empty list is considered as falsey when testing a truth value. From the python docs for Truth Value Testing (emphasis mine):

Here are most of the built-in objects considered false:

  • constants defined to be false: None and False.
  • zero of any numeric type: 0, 0.0, 0j, Decimal(0), Fraction(0, 1)
  • empty sequences and collections: '', (), [], {}, set(), range(0)

Checking of objects' truth value does not cast the object to boolean:

In [67]: lst_a, lst_b = [], [1,2,3]
    ...:
    ...: if lst_a: # The [] is falsey
    ...:     print(lst_a)
    ...: if lst_b: # Nonempty list is truthy
    ...:     print(lst_b)
    ...:
[1, 2, 3] # lst_b is still a list.

this is also mentioned in the docs with

Boolean operations or and and always return one of their operands.

Is this the Pythonic style?

I would say that yes, for example the following function

def candidates(word):
    if a := known([word]):
        return a
    elif b := know(edits1(word)):
        return b
    elif c := know(edits2(word)):
        return c
    return [word]

is "pythonic". It is readable and clear (same as you proposed, with removing the last unnecessary else and changing "elsif" to "elif").

Note that the code you linked if from 2007-2016, when there was no walrus operator (:=), since it was added in Python 3.8. In library code, the walrus operator will probably be coming later on, since based on the popular numpy package download stats, at the time of writing only roughly 7.3% of the users have Python version that supports the walrus operator.

Few words about a or b vs a | b

Note that the or operator works with many data object types (also lists), and returns the first truthy value. This is very different operator from the Bitwise operator |. Trying to do list_a | list_b will result into TypeError, and if you do bool(list_a) | bool(list_b), the outcome will be a boolean.

Related