Secrets of Logical Operators in Python

Secrets of Logical Operators in Python
12 min read

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 is True, regardless of the right operand.
  • If the left operand of the and operator is a falsy object, the overall result of the logical expression is False, 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 always True or False, but the result of the and and or operators can be any object.
  • The not operator inverts the truthiness of the operand, while the and operator returns the first falsy object or the last object, and the or 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.

In case you have found a mistake in the text, please send a message to the author by selecting the mistake and pressing Ctrl-Enter.
Den W. 2K
I'm a passionate tech enthusiast who loves diving into the world of software, programming, and tech reviews.
Comments (2)
You must be logged in to comment.

Sign In