How do "and" and "or" act with non-boolean values?
Asked Answered
M

8

148

I'm trying to learn python and came across some code that is nice and short but doesn't totally make sense

the context was:

def fn(*args):
    return len(args) and max(args)-min(args)

I get what it's doing, but why does python do this - ie return the value rather than True/False?

10 and 7-2

returns 5. Similarly, changing the and to or will result in a change in functionality. So

10 or 7 - 2

Would return 10.

Is this legit/reliable style, or are there any gotchas on this?

Mele answered 30/10, 2017 at 3:16 Comment(7)
and (as well as or) is not restricted to working with, or returning boolean values.Ullman
IMNSHO: that's a somewhat confusing way of writing that; I can't tell offhand if it's supposed to return a boolean (is there a distinct min and max) or a number (what is the difference of the min and max). If the latter, then there's also the question if it makes any sense to give that difference of a zero-length list as a number. (Instead of None or an exception)Pyles
It works, as other people have explained, however one possible issue is that if it returns 0 you can't tell whether args was empty or was nonempty but had all elements equal.Ingulf
@EspeciallyLime: exactly. I've mentioned it in my answer.Uptake
Related: Practical examples of Python AND operator.Farlee
Note: I've canonicalised the question a bit and reversed the duplicate closure because I've received feedback that this post is more helpful in explaining the concepts outlined.Ullman
Related: stackoverflow.com/questions/39983695Alica
U
200

TL;DR

We start by summarising the two behaviour of the two logical operators and and or. These idioms will form the basis of our discussion below.

and

Return the first Falsy value if there are any, else return the last value in the expression.

or

Return the first Truthy value if there are any, else return the last value in the expression.

The behaviour is also summarised in the docs, especially in this table:

Operation Result
x or y if x is false, then y, else x
x and y if x is false, then x, else y
not x if x is false, then True, else False

The only operator returning a boolean value regardless of its operands is the not operator.


"Truthiness", and "Truthy" Evaluations

The statement

len(args) and max(args) - min(args)

Is a very pythonic concise (and arguably less readable) way of saying "if args is not empty, return the result of max(args) - min(args)", otherwise return 0. In general, it is a more concise representation of an if-else expression. For example,

exp1 and exp2

Should (roughly) translate to:

r1 = exp1
if r1:
    r1 = exp2

Or, equivalently,

r1 = exp2 if exp1 else exp1

Similarly,

exp1 or exp2

Should (roughly) translate to:

r1 = exp1
if not r1:
    r1 = exp2

Or, equivalently,

r1 = exp1 if exp1 else exp2

Where exp1 and exp2 are arbitrary python objects, or expressions that return some object. The key to understanding the uses of the logical and and or operators here is understanding that they are not restricted to operating on, or returning boolean values. Any object with a truthiness value can be tested here. This includes int, str, list, dict, tuple, set, NoneType, and user defined objects. Short circuiting rules still apply as well.

But what is truthiness?
It refers to how objects are evaluated when used in conditional expressions. @Patrick Haugh summarises truthiness nicely in this post.

All values are considered "truthy" except for the following, which are "falsy":

  • None
  • False
  • 0
  • 0.0
  • 0j
  • Decimal(0)
  • Fraction(0, 1)
  • [] - an empty list
  • {} - an empty dict
  • () - an empty tuple
  • '' - an empty str
  • b'' - an empty bytes
  • set() - an empty set
  • an empty range, like range(0)
  • objects for which
    • obj.__bool__() returns False
    • obj.__len__() returns 0

A "truthy" value will satisfy the check performed by if or while statements. We use "truthy" and "falsy" to differentiate from the bool values True and False.


How and Works

We build on OP's question as a segue into a discussion on how these operators in these instances.

Given a function with the definition

def foo(*args):
    ...

How do I return the difference between the minimum and maximum value in a list of zero or more arguments?

Finding the minimum and maximum is easy (use the inbuilt functions!). The only snag here is appropriately handling the corner case where the argument list could be empty (for example, calling foo()). We can do both in a single line thanks to the and operator:

def foo(*args):
     return len(args) and max(args) - min(args)
foo(1, 2, 3, 4, 5)
# 4

foo()
# 0

Since and is used, the second expression must also be evaluated if the first is True. Note that, if the first expression is evaluated to be truthy, the return value is always the result of the second expression. If the first expression is evaluated to be Falsy, then the result returned is the result of the first expression.

In the function above, If foo receives one or more arguments, len(args) is greater than 0 (a positive number), so the result returned is max(args) - min(args). OTOH, if no arguments are passed, len(args) is 0 which is Falsy, and 0 is returned.

Note that an alternative way to write this function would be:

def foo(*args):
    if not len(args):
        return 0
    
    return max(args) - min(args)

Or, more concisely,

def foo(*args):
    return 0 if not args else max(args) - min(args)

If course, none of these functions perform any type checking, so unless you completely trust the input provided, do not rely on the simplicity of these constructs.


How or Works

I explain the working of or in a similar fashion with a contrived example.

Given a function with the definition

def foo(*args):
    ...

How would you complete foo to return all numbers over 9000?

We use or to handle the corner case here. We define foo as:

def foo(*args):
     return [x for x in args if x > 9000] or 'No number over 9000!'

foo(9004, 1, 2, 500)
# [9004]

foo(1, 2, 3, 4)
# 'No number over 9000!'

foo performs a filtration on the list to retain all numbers over 9000. If there exist any such numbers, the result of the list comprehension is a non-empty list which is Truthy, so it is returned (short circuiting in action here). If there exist no such numbers, then the result of the list comp is [] which is Falsy. So the second expression is now evaluated (a non-empty string) and is returned.

Using conditionals, we could re-write this function as,

def foo(*args):
    r = [x for x in args if x > 9000]
    if not r:
        return 'No number over 9000!' 
    
    return r

As before, this structure is more flexible in terms of error handling.

Ullman answered 30/10, 2017 at 3:28 Comment(15)
@Mele You're welcome. They're certainly useful constructs to keep in mind, and can drastically shorten your code. I find myself using them all the time. You can use it anywhere in fact, not just in return statements. Assignments, conditionals, and so on.Ullman
It is not "pythonic" to sacrifice all clarity for brevity, which I think is the case here. It's not a straightforward construct.Latoria
I think one should note that Python conditional expressions have made this syntax less common. I certainly prefer max(args) - min(args) if len(args) else 0 to the original.Brazilin
Another common one that is confusing at first, is assigning a value if none exists: "some_var = arg or 3"Tamalatamale
@Brazilin I was thinking the exact same thing reading this. I like "terse but clear" code.Blindly
@Blindly before people start bashing this syntax in favour of ternary operators, keep in mind that when it comes to n-ary condition expressions, ternary operators can get out of hand quickly. For example, if ... else (if ... else (if ... else (if ... else ...))) can just as well be rewritten as ... and ... and ... and ... and ... and at that point it really becomes hard to argue readability for either case.Ullman
return step1(...) and step2(...) and ... and last_step() is not a terribly bad way to sequence actions that may fail in C, but Python has actual exceptions.Srinagar
@richardb: You can omit len in your ternary.Uptake
There's an else missing in your explanation of if args is not empty, return the result of max(args) - min(args).. In the above example, I think that throwing an exception according to the EAFP principle would be a better choice than a logical and or a ternary.Uptake
@EricDuminil right... I didn’t want to complicate things. I do mention the else in all other places. Alternatively, the EAFP is more readable and I certainly agree with you there.Ullman
It's not pythonic to sacrifice clarity for brevity, but this doesn't do so. It's a well known idiom. It's an idiom you have to learn, like any other idiom, but it's hardly 'sacrificing clarity'.Loper
Isn't [] == False? So couldn't you just args and ...?Hodgkins
@Hodgkins In the case of args being empty, you'd want 0 returned, not [].Ullman
Your explanation is incorrect. exp1 and exp2 roughly translates to r1 = exp1 if r1: r1 = exp2.Octagonal
In case anyone is wondering, both not and the comparison operators in Python always return a pure boolean value of True or False (examples: x < 5 or chained like 0 < x <= 1 or even 5 > x < y; chained comparisons finish before any boolean gets returned).Groark
P
21

Quoting from Python Docs

Note that neither and nor or restrict the value and type they return to False and True, but rather return the last evaluated argument. This is sometimes useful, e.g., if s is a string that should be replaced by a default value if it is empty, the expression s or 'foo' yields the desired value.

So, this is how Python was designed to evaluate the boolean expressions and the above documentation gives us an insight of why they did it so.

To get a boolean value just typecast it.

return bool(len(args) and max(args)-min(args))

Why?

Short-circuiting.

For example:

2 and 3 # Returns 3 because 2 is Truthy so it has to check 3 too
0 and 3 # Returns 0 because 0 is Falsey and there's no need to check 3 at all

The same goes for or too, that is, it will return the expression which is Truthy as soon as it finds it, cause evaluating the rest of the expression is redundant.

Instead of returning hardcore True or False, Python returns Truthy or Falsey, which are anyway going to evaluate to True or False. You could use the expression as is, and it will still work.


To know what's Truthy and Falsey, check Patrick Haugh's answer

Primaveria answered 30/10, 2017 at 3:23 Comment(0)
D
8

and and or perform boolean logic, but they return one of the actual values when they are comparing. When using and, values are evaluated in a boolean context from left to right. 0, '', [], (), {}, and None are false in a boolean context; everything else is true.

If all values are true in a boolean context, and returns the last value.

>>> 2 and 5
5
>>> 2 and 5 and 10
10

If any value is false in a boolean context and returns the first false value.

>>> '' and 5
''
>>> 2 and 0 and 5
0

So the code

return len(args) and max(args)-min(args)

returns the value of max(args)-min(args) when there is args else it returns len(args) which is 0.

Dagney answered 30/10, 2017 at 3:33 Comment(0)
J
5

Is this legit/reliable style, or are there any gotchas on this?

This is legit, it is a short circuit evaluation where the last value is returned.

You provide a good example. The function will return 0 if no arguments are passed, and the code doesn't have to check for a special case of no arguments passed.

Another way to use this, is to default None arguments to a mutable primitive, like an empty list:

def fn(alist=None):
    alist = alist or []
    ....

If some non-truthy value is passed to alist it defaults to an empty list, handy way to avoid an if statement and the mutable default argument pitfall

Jillion answered 30/10, 2017 at 3:26 Comment(0)
U
3

Gotchas

Yes, there are a few gotchas.

fn() == fn(3) == fn(4, 4)

First, if fn returns 0, you cannot know if it was called without any parameter, with one parameter or with multiple, equal parameters :

>>> fn()
0
>>> fn(3)
0
>>> fn(3, 3, 3)
0

What does fn mean?

Then, Python is a dynamic language. It's not specified anywhere what fn does, what its input should be and what its output should look like. Therefore, it's really important to name the function correctly. Similarly, arguments don't have to be called args. delta(*numbers) or calculate_range(*numbers) might describe better what the function is supposed to do.

Argument errors

Finally, the logical and operator is supposed to prevent the function to fail if called without any argument. It still fails if some argument isn't a number, though:

>>> fn('1')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in fn
TypeError: unsupported operand type(s) for -: 'str' and 'str'
>>> fn(1, '2')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in fn
TypeError: '>' not supported between instances of 'str' and 'int'
>>> fn('a', 'b')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in fn
TypeError: unsupported operand type(s) for -: 'str' and 'str'

Possible alternative

Here's a way to write the function according to the "Easier to ask for forgiveness than permission." principle:

def delta(*numbers):
    try:
        return max(numbers) - min(numbers)
    except TypeError:
        raise ValueError("delta should only be called with numerical arguments") from None
    except ValueError:
        raise ValueError("delta should be called with at least one numerical argument") from None

As an example:

>>> delta()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in delta
ValueError: delta should be called with at least one numerical argument
>>> delta(3)
0
>>> delta('a')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in delta
ValueError: delta should only be called with numerical arguments
>>> delta('a', 'b')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in delta
ValueError: delta should only be called with numerical arguments
>>> delta('a', 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in delta
ValueError: delta should only be called with numerical arguments
>>> delta(3, 4.5)
1.5
>>> delta(3, 5, 7, 2)
5

If you really don't want to raise an exception when delta is called without any argument, you could return some value which cannot be possible otherwise (e.g. -1 or None):

>>> def delta(*numbers):
...     try:
...         return max(numbers) - min(numbers)
...     except TypeError:
...         raise ValueError("delta should only be called with numerical arguments") from None
...     except ValueError:
...         return -1 # or None
... 
>>> 
>>> delta()
-1
Uptake answered 30/10, 2017 at 14:58 Comment(0)
V
0

Yes. This is the correct behaviour of and comparison.

At least in Python, A and B returns B if A is essentially True including if A is NOT Null, NOT None NOT an Empty container (such as an empty list, dict, etc). A is returned IFF A is essentially False or None or Empty or Null.

On the other hand, A or B returns A if A is essentially True including if A is NOT Null, NOT None NOT an Empty container (such as an empty list, dict, etc), otherwise it returns B.

It is easy to not notice (or to overlook) this behaviour because, in Python, any non-null non-empty object evaluates to True is treated like a boolean.

For example, all the following will print "True"

if [102]: 
    print "True"
else: 
    print "False"

if "anything that is not empty or None": 
    print "True"
else: 
    print "False"

if {1, 2, 3}: 
    print "True"
else: 
    print "False"

On the other hand, all the following will print "False"

if []: 
    print "True"
else: 
    print "False"

if "": 
    print "True"
else: 
    print "False"

if set ([]): 
    print "True"
else: 
    print "False"
Vuong answered 30/10, 2017 at 3:27 Comment(1)
Thank you. I wanted to write A is essentially True. Corrected.Vuong
C
0

Is this legit/reliable style, or are there any gotchas on this?

I would like to add to this question that it not only legit and reliable but it also ultra practical. Here is a simple example:

>>>example_list = []
>>>print example_list or 'empty list'
empty list

Therefore you can really use it at your advantage. In order to be conscise this is how I see it:

Or operator

Python's or operator returns the first Truth-y value, or the last value, and stops

And operator

Python's and operator returns the first False-y value, or the last value, and stops

Behind the scenes

In python, all numbers are interpreted as True except for 0. Therefore, saying:

0 and 10 

is the same as:

False and True

Which is clearly False. It is therefore logical that it returns 0

Craquelure answered 30/10, 2017 at 3:33 Comment(0)
V
0

to understand in simple way,

AND : if first_val is False return first_val else second_value

eg:

1 and 2 # here it will return 2 because 1 is not False

but,

0 and 2 # will return 0 because first value is 0 i.e False

and => if anyone false, it will be false. if both are true then only it will become true

OR : if first_val is False return second_val else first_value

reason is, if first is false it check whether 2 is true or not.

eg:

1 or 2 # here it will return 1 because 1 is not False

but,

0 or 2 # will return 2 because first value is 0 i.e False

or => if anyone false, it will be true. so if first value is false no matter what 2 value suppose to be. so it returns second value what ever it can be.

if anyone is true then it will become true. if both are false then it will become false.

Vinaya answered 19/6, 2019 at 6:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.