Logical operations play an important role in programming. They are used to create conditional constructs and compose complex algorithms. In Python, logical operations are performed using logical operators:
-
not
— logical negation -
and
— logical conjunction -
or
— logical disjunction
In this article, we will talk about the non-obvious details and hidden features of how logical operators work in Python.
Truth Tables for Logical Operators
We are accustomed to logical operators in programming languages returning True
or False
values according to their truth tables.
Truth Table for the not
Operator:
a |
not a |
---|---|
False | True |
True | False |
Truth Table for the and
Operator:
a |
b |
a and b |
---|---|---|
False | False | False |
False | True | False |
True | False | False |
True | True | True |
Truth Table for the or
Operator:
a |
b |
a or b |
---|---|---|
False | False | False |
False | True | True |
True | False | True |
True | True | True |
When the operands of logical operators are True
and False
, the behavior of logical operators in Python also follows these truth tables.
print(not True)
print(not False)
print(False and True)
print(True and True)
print(False or True)
print(False or False)
This code outputs:
False
True
False
True
True
False
However, Python does not restrict us to just True
and False
as operands for logical operators. The operands of the not
, and
, and or
operators can be objects of any other data types.
Concepts of Truthy and Falsy
One of the important features of Python is the concept of truthy and falsy objects. Any object in Python can be evaluated as True
or False
. Objects that evaluate to True
are called truthy objects, while objects that evaluate to False
are called falsy objects.
Built-in falsy objects include:
- The value
False
- The value
None
- Numeric zeros:
0
,0.0
,0j
,Decimal(0)
,Fraction(0, 1)
- Empty sequences and collections:
''
,()
,[]
,{}
,set()
,range(0)
Other objects of built-in data types are truthy objects. Instances of custom classes are also truthy objects by default.
To convert objects to True
or False
, the built-in bool()
function is used.
# Falsy objects
print(bool(False))
print(bool(None))
print(bool(0))
print(bool(0.0))
print(bool([]))
print(bool(''))
print(bool({}))
# Truthy objects
print(bool(True))
print(bool(123))
print(bool(69.96))
print(bool('beegeek'))
print(bool([4, 8, 15, 16, 23, 42]))
print(bool({1, 2, 3}))
This code outputs:
False
False
False
False
False
False
False
True
True
True
True
True
True
The concept of truthy and falsy objects in Python allows for working with conditional operators in a simpler manner.
For example, the following code:
if len(data) > 0:
...
if value == True:
...
if value == False:
...
can be rewritten as:
if data:
...
if value:
...
if not value:
...
The not
Operator
As we know, the operand of the not
operator can be an object of any type. If the operand is different from True
and False
, it is evaluated according to the concept of truthy and falsy objects. The result of the not
operator is always True
or False
.
print(not False)
print(not None)
print(not 0)
print(not 0.0)
print(not [])
print(not '')
print(not {})
This code outputs:
True
True
True
True
True
True
True
print(not True)
print(not 123)
print(not 69.96)
print(not 'beegeek')
print(not [4, 8, 15, 16, 23, 42])
print(not {1, 2, 3})
This code outputs:
False
False
False
False
False
False
The and
and or
Operators
The operands of the and
and or
operators, like the not
operator, can be objects of any data types. Analogous to the not
operator, you might assume that the result of the and
and or
operators is also True
or False
. However, these operators return one of their operands. Which one depends on the operator itself.
print(None or 0)
print(0 or 5)
print('beegeek' or None)
print([1, 2, 3] or [6, 9])
print(1 or 'beegeek' or None)
print(0.0 or 'habr' or {'one': 1})
print(0 or '' or [6, 9])
print(0 or '' or [])
print(0 or '' or [] or {})
This code outputs:
0
5
beegeek
[1, 2, 3]
1
habr
[6, 9]
[]
{}
As we can see, the or
operator evaluates each operand as a truthy or falsy object but returns not True
or False
, but the first truthy object or the last object if no truthy objects are found in the logical expression.
Similarly, the and
operator behaves in the same way.
print(None and 10)
print(5 and 0.0)
print('beegeek' and {})
print([1, 2, 3] and [6, 9])
print(1 and 'beegeek' and None)
print('habr' and 0 and {'one': 1})
print(10 and [6, 9] and [])
This code outputs:
None
0.0
{}
[6, 9]
None
0
[]
The and
operator returns the first falsy object or the last object if no falsy objects are found in the logical expression.
Logical Operators Are Lazy
Logical operators in Python are lazy. This means that the returned operand is determined by evaluating the truthiness of all operands from left to right until it becomes irrelevant:
- If the left operand of the
or
operator is a truthy object, the overall result of the logical expression isTrue
, regardless of the right operand. - If the left operand of the
and
operator is a falsy object, the overall result of the logical expression isFalse
, regardless of the right operand.
This mechanism is called short-circuit evaluation and is used by the interpreter to optimize computations. Consider the following example that demonstrates this behavior.
def f():
print('bee')
return 3
if True or f():
print('geek')
This code outputs:
geek
The left operand of the or
operator is a truthy object (True
), so there is no need to evaluate the right operand, which means calling the f()
function. Since the function is not called, the output does not include the string bee
. The overall result of the logical expression is True
, so the instructions in the conditional block are executed, and we see the string geek
in the output.
In contrast, the following code:
def f():
print('bee')
return 3
if True and f():
print('geek')
This code outputs:
bee
geek
The left operand of the and
operator is a truthy object (True
), so the right operand needs to be evaluated, which means calling the f()
function. As a result, the function's instructions are executed, and we see the string bee
in the output. The function returns the number 3
, which is also a truthy object. Therefore, the overall result of the logical expression is 3
, and the instructions in the conditional block are executed, and we see the string geek
in the output.
Priority of Logical Operators
It is important to remember the priority of logical operators. Below, the logical operators are listed in decreasing order of priority (top to bottom):
-
not
-
and
-
or
According to the priority of logical operators, the following code:
a = 0
b = 7
c = 10
print(not a and b or not c) # 7
is equivalent to:
a = 0
b = 7
c = 10
print(((not a) and b) or (not c)) # 7
Relative to other Python operators (
for example, arithmetic operators), logical operators have the lowest priority. If you want to change the priority of operators, use parentheses.
For example, the following code:
a = 1
b = 2
c = 3
d = 4
print(a + b and c + d) # 7
is equivalent to:
a = 1
b = 2
c = 3
d = 4
print((a + b) and (c + d)) # 7
But the following code:
a = 1
b = 2
c = 3
d = 4
print(a + (b and c) + d) # 5
is equivalent to:
a = 1
b = 2
c = 3
d = 4
print(a + (b and c) + d) # 5
The Peculiarities of Logical Operators
If the operands of logical operators are complex objects, the result may be unexpected. Let's look at an example:
d1 = {'a': 1}
d2 = {'b': 2}
result = d1 or d2
print(result) # {'a': 1}
result['a'] += 10
print(d1) # {'a': 11}
As we can see, the or
operator returns the first truthy object (d1
) and assigns it to the result
variable. If we then change the value by the key 'a'
in the result
dictionary, the dictionary d1
will also change, as both variables refer to the same object.
If we rewrite the example in a more concise way, it becomes clear why this happens:
d1 = {'a': 1}
d2 = {'b': 2}
d = d1 if d1 else d2
d['a'] += 10
print(d1) # {'a': 11}
In a similar way, the behavior of the and
operator can be demonstrated:
d1 = {'a': 1}
d2 = {'b': 2}
result = d1 and d2
print(result) # {'b': 2}
result['b'] += 10
print(d2) # {'b': 12}
The and
operator returns the second object if the first is a truthy object. If the value by the key 'b'
is then changed in the result
dictionary, the d2
dictionary will also change, as both variables refer to the same object.
In a more concise form:
d1 = {'a': 1}
d2 = {'b': 2}
d = d2 if d1 else d1
d['b'] += 10
print(d2) # {'b': 12}
Summary
In Python, logical operators are used to create conditional constructs and compose complex algorithms. However, they also have non-obvious details and hidden features:
- The result of the
not
operator is alwaysTrue
orFalse
, but the result of theand
andor
operators can be any object. - The
not
operator inverts the truthiness of the operand, while theand
operator returns the first falsy object or the last object, and theor
operator returns the first truthy object or the last object. - Logical operators in Python are lazy, meaning they only evaluate operands until the overall result of the logical expression becomes evident.
- Logical operators have a specific priority relative to each other and other Python operators.
- The behavior of logical operators can be unexpected when their operands are complex objects, as the operators return references to the objects, not their copies.
Understanding the peculiarities of logical operators in Python allows for writing more effective and efficient code.
Comments (2)