Expressions and operators · about 40 min

Comparisons, boolean logic and truthiness

Why this matters

Every decision a program makes, whether to retry a request, which branch of code to run, when to stop a loop, comes down to an expression that is true or false. Python's comparison and logic operators are richer than they first look: comparisons can be chained like in math, and and or stop early and return actual values rather than just True/False, and every object can be used as a condition. These behaviors power common idioms such as defaults (name or "stranger") and guards (b != 0 and a % b == 0). Next module, if and while will put them to work.

Comparison operators

OperatorMeaning
==equal value
!=not equal
<, <=less than, less or equal
>, >=greater than, greater or equal
is, is notsame object, different object
in, not inmembership (for strings, lists, ...)

Each returns True or False:

print(3 == 3.0)        # int and float compare by value
print("abc" == "ABC")  # case matters
print("apple" < "banana")
print("Zebra" < "apple")
print("ell" in "hello")
True
False
True
True
True

Strings compare lexicographically by character code, like a dictionary but with all uppercase letters (codes 65-90) before all lowercase ones (97-122). That is why "Zebra" < "apple". Comparing incompatible types with < is an error: 3 < "4" raises TypeError: '<' not supported between instances of 'int' and 'str'. (== between unrelated types is allowed and simply returns False.)

Chained comparisons

Python lets you chain comparisons the way math does:

x = 15
print(10 <= x < 20)
print(1 < 2 < 3 < 4)
True
True

a < b < c means a < b and b < c, except that b is evaluated only once. Any comparison operators can be chained, which occasionally reads strangely: 1 < 3 > 2 is True because it means 1 < 3 and 3 > 2. Keep chains to the natural "in range" shape.

Logical operators: and, or, not

ExpressionValue
not xTrue if x is falsy, else False
x and yif x is falsy, x; otherwise y
x or yif x is truthy, x; otherwise y

With booleans they behave like the truth tables you might know:

print(True and False, True or False, not True)
False True False

Precedence, lowest to highest: or, then and, then not, all below comparisons. So a < b and not c == d or e groups as ((a < b) and (not (c == d))) or e. Use parentheses when mixing and with or.

Truthiness

Any object can be used where a true/false value is expected. Python asks the object for its truth value, as bool(x) would. The built-in rule, from the conversion lesson:

falsy:   False   None   0   0.0   ""   []   {}   set()   (and other empty containers)
truthy:  everything else
print(not "")
print(not "hello")
print(not 0, not 7)
True
False
True False

Truthiness lets you write if name: instead of if name != "":, and if items: instead of if len(items) > 0:. Both idioms are standard Python.

and/or return operands, and short-circuit

This is the part that surprises people. and and or do not convert their result to a bool. They return one of the operands, and they evaluate the right operand only if needed:

print(0 or 5)
print(3 or 5)
print(0 and 5)
print(3 and 5)
print("" or "default")
5
3
0
5
default

Not evaluating the right side when the answer is already known is called short-circuit evaluation. In the bytecode, it is a conditional jump: CPython evaluates the left operand, and if it already decides the result, jumps past the code for the right operand. You can see this with dis.dis("a or b"): there is a POP_JUMP_IF_TRUE-style instruction that skips LOAD_NAME b.

Short-circuiting gives two important idioms.

Defaults with or:

name = ""
display = name or "anonymous"
print(display)
anonymous

Careful: or treats every falsy value as missing. If 0 is a legitimate value (count = given or 10 with given = 0), you get 10 instead of 0. In those cases test explicitly with is None (using if, next module).

Guards with and: put a cheap safety check on the left so a dangerous operation on the right only runs when safe.

a, b = 10, 0
print(b != 0 and a / b > 2)
False

Without the guard, a / b would raise ZeroDivisionError. Because b != 0 is False, and returns False and never evaluates the division. Order matters: a / b > 2 and b != 0 crashes.

Equality versus identity, again

== compares values, is compares objects (lesson 2 of the previous module). For None, use is. For everything else, use ==:

x = None
print(x is None)
print([1, 2] == [1, 2], [1, 2] is [1, 2])
True
True False

Common mistakes

Recap

Going further: run import dis; dis.dis("a and b or c") and trace the jump instructions. Read the Python reference section "Boolean operations".

Exercises

Exercise 1 · quiz

What do and/or return?

What does this print?

print(0 or "default", "" and 5, 3 and 4)
Answer
  • default 4

and and or return one of their operands, not necessarily a bool. 0 or "default": 0 is falsy, so or returns the second operand, "default". "" and 5: "" is falsy, so and stops there and returns "" (printed as nothing, hence the double space). 3 and 4: 3 is truthy, so and returns the second operand, 4.

Exercise 2 · quiz

Chained comparisons

Select all expressions that evaluate to True.

Answer
  • 1 < 3 > 2

  • 5 > 4 > 3 > 2

  • 1 < 2 == 2.0

  • 0 <= 0 < 1

a < b > c means a < b and b > c, each middle operand shared by its neighbors. 1 < 3 > 2 is 1 < 3 and 3 > 2: True. 1 < 2 == 2.0 is 1 < 2 and 2 == 2.0: True (int and float compare by value). 3 > 2 > 1 == False means 3 > 2 and 2 > 1 and 1 == False, and 1 == False is False, so the whole chain is False. It does not mean (3 > 2 > 1) == False.

Exercise 3 · write code

Leap years

A year is a leap year if it is divisible by 4, except that years divisible by 100 are not, except that years divisible by 400 are. Read a year and print exactly (for 2024):

2024 leap: True

Use a single boolean expression with and, or and %; no if needed.

File: main.py

Starting point

year = int(input())
print(year, "leap:", year % 4 == 0)

Example

input

2024

expected output

2024 leap: True

Example

input

1900

expected output

1900 leap: False
Hint 1

year % 4 == 0 is True when 4 divides the year.

Hint 2

Divisible by 4 but not by 100: year % 4 == 0 and year % 100 != 0.

Hint 3

Combine that with or year % 400 == 0.

Reference solution
year = int(input())
leap = (year % 4 == 0 and year % 100 != 0) or year % 400 == 0
print(year, "leap:", leap)

Exercise 4 · write code

A default with or

Read one line. If it is non-empty, greet that name; if the line is empty, greet stranger. Output exactly (for input Ada):

Hello, Ada!

and for an empty line:

Hello, stranger!

Do it without if, using the fact that or returns its second operand when the first is falsy.

File: main.py

Starting point

name = input()
print("Hello, " + name + "!")

Example

input

Ada

expected output

Hello, Ada!

Example

input

expected output

Hello, stranger!
Hint 1

An empty string is falsy.

Hint 2

name or "stranger" is name if it is non-empty, otherwise "stranger".

Hint 3

Parenthesize the or expression: + binds more tightly than or.

Reference solution
name = input()
print("Hello, " + (name or "stranger") + "!")

Exercise 5 · write code

Short-circuit as a guard

Read two whole numbers a and b. Print whether a is divisible by b. Zero divides nothing, so if b is 0 the answer is False, and the program must not crash. Output exactly (for 12 and 4):

divisible: True

The starter crashes with ZeroDivisionError when b is 0. Fix it with a guard using and, so the % is never evaluated when b is 0.

File: main.py

Starting point

a = int(input())
b = int(input())
print("divisible:", a % b == 0)

Example

input

12
4

expected output

divisible: True

Example

input

12
5

expected output

divisible: False
Hint 1

and stops at the first falsy operand and returns it.

Hint 2

Put the check that b is not zero on the left: b != 0 and ....

Reference solution
a = int(input())
b = int(input())
print("divisible:", b != 0 and a % b == 0)