---
title: Conditionals
source: 'https://academia.sh/en/courses/python-fundamentals/conditionals'
course: 'Python Fundamentals'
language: en
updated: '2026-08-17T18:10:29+00:00'
license: 'CC BY-SA 4.0'
---

# Conditionals

A condition does not expect a logical value, it puts the object through the truthiness rule: eight protocol calls are measured across twelve trials, four return true without calling any method, and a comparison chain produces its middle term once.

The previous lesson measured type conversion and showed that where conversion fails,
an **exception** is born: when an object cannot answer for the type demanded of it,
flow is interrupted. Conversion asks the object "what do you correspond to as an
integer?" and that question has the right to go unanswered.

A condition also asks the object a question, but a different one: **"are you counted
as true?"** Unlike conversion's, the answer here is almost never an exception — if
the object does not give the answer itself, the language produces one in its place.
So who gives that answer? Does `if` look inside the object and count its items, check
its type, or ask the object? This is what this lesson measures.

## A Condition Does Not Expect a Logical Value

The Programming Fundamentals course's conditional-branching lesson defined a
condition as **an expression producing a logical value**; that definition was
language-independent and built branching as a **control structure**. In Python the
definition is broader: **any object** can be written after the `if` keyword, and the
language reduces it to a truth value itself. What is measured here is not branching
itself, but **which special method** that reduction calls.

The reduction rule is called **truthiness** and has three rungs:

1. If `__bool__` is defined on the object, **it is called**; the value it returns is
   the result itself.
2. If not defined, `__len__` **is called**; if the result is greater than zero, the
   object is counted true.
3. If neither is defined, no method is called at all and the object is **always
   true**.

The third rung is the most overlooked part of the rule. An empty object being counted
false is not a language rule, it is **the object's own declaration**: if there is no
method declaring it, there is no concept of emptiness either. An ordinary object,
whatever it contains, is true.

## A Chain Is a Shorthand

Comparison operators are chainable in Python: `a < b < c` is a valid expression. This
notation does **not** mean a left-to-right pairwise comparison — if it did, `a < b`
would produce a truth value, that value would be compared with `c`, and the result
would come out meaningless.

The chain means the expression `a < b and b < c`, with two differences. First, the
middle term `b` is evaluated **once**. In the expanded notation `b` is written twice,
and if it is a function call, it runs twice. Second, like `and`, a chain also
**short-circuits**: if the first comparison comes out false, the second is never even
built, and the right-hand term is not even produced.

The chaining rule applies not only to `<` and `>` but to **every comparison
operator**; membership and identity operators are in the same class. This is why
`a is b is c` is as valid a chain as `a < b <= c` and carries the same two
guarantees. A chain has no length limit: each additional term builds its own
comparison with the one before it, and never comes up unless everything to its left
has come out true.

Neither of these is visible to the eye. The only way to see how many times terms are
produced is to build a rig that counts production.

## What Short-Circuiting Returns

The `and` and `or` operators do not return a logical value; they return **one of the
operands**. The expression `x or y` gives `x` if `x` is true, `y` otherwise. What is
returned is the object itself, not its truth value.

The protocol the two call is separate too: `and` and `or` do not call their own
special method on the operands the way an arithmetic operator does. All they do is put
**the left operand through the truthiness rule** and decide, based on the result,
which operand to return. The right operand is returned if needed — it is not tested.

The measurement's assumptions:

- **CF1** — The oracle is the rig itself: which special method gets called is known
  because the object records it itself; the measurement is not an outside
  observation, it is the object's own declaration.
- **CF2** — Three classes are used. `Tracker` defines only `__len__`, `NonEmpty` adds
  `__bool__` to it, `Silent` defines neither. The three classes correspond exactly to
  the three rungs of the truthiness rule.
- **CF3** — Each class is tested in an empty and a three-item state; the item count
  changes not to change the result, but to distinguish **who determines the result**.
- **CF4** — The chain's middle term is produced by a function call, and every
  production is logged; this log is the measure for the question "how many times was
  it evaluated."
- **CF5** — `__lt__` compares by length. **What** the comparison is based on does not
  enter the measurement; what is measured is **how many times** it is called.
- **CF6** — `Tracker` defines only `__lt__`, its counterpart `__gt__` is deliberately
  left undefined, so that reflection can be measured.
- **CF7** — The calls counted come only from the forms this lesson builds; other forms
  in the shared reference are not part of this measurement.

## Measurement

```python
"""Truthiness: which special method does `if n` call, in what order."""

LOG = []


def record(name):
    LOG.append(name)


class Tracker:
    """__len__ defined, __bool__ undefined."""

    def __init__(self, items=(1, 2, 3)):
        self.items = list(items)

    def __len__(self):
        record("__len__")
        return len(self.items)

    def __lt__(self, other):
        record("__lt__")
        return len(self.items) < len(other.items)


class NonEmpty(Tracker):
    """__bool__ defined: truthiness does not fall to __len__."""

    def __bool__(self):
        record("__bool__")
        return len(self.items) > 0


class Silent:
    """Neither __bool__ nor __len__ defined."""

    def __init__(self, items=()):
        self.items = list(items)


def measure(action):
    LOG.clear()
    try:
        value = action()
    except Exception as e:
        LOG.append(f"!{type(e).__name__}")
        value = None
    return list(LOG), value


OBJECTS = (
    ("Tracker(3 items)", lambda: Tracker((1, 2, 3))),
    ("Tracker(0 items)", lambda: Tracker(())),
    ("NonEmpty(3 items)", lambda: NonEmpty((1, 2, 3))),
    ("NonEmpty(0 items)", lambda: NonEmpty(())),
    ("Silent(3 items)", lambda: Silent((1, 2, 3))),
    ("Silent(0 items)", lambda: Silent(())),
)

print(f"{'object':<19s} {'if n':>6s} {'not n':>6s}  protocol called")
total = trials = silent = 0
for name, build in OBJECTS:
    n = build()
    c1, d1 = measure(lambda: bool(n))
    c2, d2 = measure(lambda: not n)
    total += len(c1) + len(c2)
    trials += 2
    silent += (len(c1) == 0) + (len(c2) == 0)
    print(f"  {name:<17s} {str(d1):>6s} {str(d2):>6s}  {' '.join(c1) or '(no method)'}")
print(f"\ntrials {trials}, calls {total}, calling no method {silent}")

PRODUCED = []


def produce(name, n):
    PRODUCED.append(name)
    return Tracker(range(n))


def chained():
    return produce("a", 1) < produce("b", 2) < produce("c", 3)


def expanded():
    return (produce("a", 1) < produce("b", 2)) and (produce("b", 2) < produce("c", 3))


def short_circuit():
    return produce("a", 3) < produce("b", 2) < produce("c", 9)


print()
print(f"{'form':<17s} {'result':>6s} {'__lt__':>7s} {'produced':>9s}  order")
for name, f in (("a<b<c", chained), ("a<b and b<c", expanded),
                ("a<b<c  (false)", short_circuit)):
    PRODUCED.clear()
    c, d = measure(f)
    print(f"  {name:<15s} {str(d):>6s} {c.count('__lt__'):7d}"
          f" {len(PRODUCED):9d}  {' '.join(PRODUCED)}")

print()
print(f"{'comparison':<25s} {'result':>6s}  protocol called")
for name, f in (("Tracker(1) < Tracker(2)", lambda: Tracker((1,)) < Tracker((1, 2))),
              ("Tracker(2) > Tracker(1)", lambda: Tracker((1, 2)) > Tracker((1,))),
              ("Silent(1) < Silent(2)", lambda: Silent((1,)) < Silent((1, 2)))):
    c, d = measure(f)
    print(f"  {name:<23s} {str(d):>6s}  {' '.join(c)}")

print()
print(f"{'expression':<32s} {'calls':>6s}  {'returned type':<16s} protocol")
EMPTY, FULL = Tracker(()), NonEmpty((1, 2))
for name, f in (("Tracker(0) or NonEmpty(2)", lambda: EMPTY or FULL),
              ("NonEmpty(2) or Tracker(0)", lambda: FULL or EMPTY),
              ("Tracker(0) and NonEmpty(2)", lambda: EMPTY and FULL),
              ("bool(Tracker(0) or Tracker(0))", lambda: bool(Tracker(()) or Tracker(())))):
    c, d = measure(f)
    print(f"  {name:<30s} {len(c):6d}  {type(d).__name__:<16s} {' '.join(c)}")
```

```
object                if n  not n  protocol called
  Tracker(3 items)    True  False  __len__
  Tracker(0 items)   False   True  __len__
  NonEmpty(3 items)   True  False  __bool__
  NonEmpty(0 items)  False   True  __bool__
  Silent(3 items)     True  False  (no method)
  Silent(0 items)     True  False  (no method)

trials 12, calls 8, calling no method 4

form              result  __lt__  produced  order
  a<b<c             True       2         3  a b c
  a<b and b<c       True       2         4  a b b c
  a<b<c  (false)   False       1         2  a b

comparison                result  protocol called
  Tracker(1) < Tracker(2)   True  __lt__
  Tracker(2) > Tracker(1)   True  __lt__
  Silent(1) < Silent(2)     None  !TypeError

expression                        calls  returned type    protocol
  Tracker(0) or NonEmpty(2)           1  NonEmpty         __len__
  NonEmpty(2) or Tracker(0)           1  NonEmpty         __bool__
  Tracker(0) and NonEmpty(2)          1  Tracker          __len__
  bool(Tracker(0) or Tracker(0))      2  bool             __len__ __len__
```

## The Cost of the Three Rungs

The top table lays the truthiness rule's three rungs side by side. There are **eight
calls across twelve trials**; the remaining four trials produce a result **without
calling any method**.

The first two rows are the second rung: because `Tracker` does not define `__bool__`,
the trial falls to `__len__`, and the item count determines the result. The third and
fourth rows are the first rung: the moment `__bool__` is added to the same class,
`__len__` is **never called at all**. When both are defined, there is no question of
which method gets called — if `__bool__` exists, `__len__` takes no part in the
trial.

The last two rows are the third rung, and this is where the table's real result
shows. `Silent(0 items)` is **empty**, but the trial gives **true** — and it does this
not by calling a method, but by **calling none at all**. Whether the object has three
items or zero does not change the result, because there is no method to produce the
result. The practical consequence: a program testing an empty instance of its own
class with `if`, if it forgot to write `__len__` or `__bool__`, always takes the
**true** branch, and nothing signals that this happened.

The `not n` column shows the same chain running once more: negation is not a separate
protocol, it is the reverse of the same trial's result. The only difference between
the columns shows up in the `Silent` rows — `not` returns not an object, but a real
logical value.

## How Many Terms a Chain Produces

The middle table shows in numbers that a chain is a shorthand. `a<b<c` and the
expanded notation call `__lt__` the same number of times: **both 2**. There is no
gain in comparison count.

The difference is in the produced column. The chain produces three terms, the
expanded notation **four**: `b` is produced twice. This is a difference not of
readability but of **meaning**. If the middle term is a function call, that function
runs twice; if it has a side effect, the side effect happens twice; if it returns a
different value on each call, the two comparisons test **two separate values unaware
of each other**. The expanded notation does not give the guarantee the chain gives.

The third row measures short-circuiting. Because the first comparison comes out
false, `__lt__` is called **1** time and production stops at **2**: `c` is never
built at all. The term at the chain's right end is something that will not be
produced without looking to its left.

## The Protocol Found in the Reverse Direction

The third table shows comparison's own fallback rule, and the middle row is the
lesson's sharpest result. The `Tracker` class defines `__lt__`, not `__gt__`. Yet
`Tracker(2) > Tracker(1)` runs — and the method called is **`__lt__`**.

The rule is this: if the left operand's method cannot be found in a comparison, the
language tries the **right operand's counterpart method**. For `a > b`, `__gt__` is
first sought on `a`; if absent, `__lt__` is called on `b` and the operands swap
places. The result comes out correct, because "`b` is less than `a`" and "`a` is
greater than `b`" are the same claim.

The direct consequence of this is the second claim itself: **the operator written
does not say the name of the method called.** Two rows give `True`, both call the
same method, but one writes `<` and the other `>`. It is impossible to guess which
method ran by looking at the syntax; only measurement tells you.

The third row shows where the rule ends. `Silent` defines neither `__lt__` nor
`__gt__`; no method is found on either the left or the right operand, and the
comparison produces not a result but a `TypeError`. While the third rung of the
truthiness trial silently returns true, comparison's last rung ends in an
**exception**: one syntax has a default behavior, the other does not. An object with
no protocol does not carry that syntax at all.

## The Condition Statement and the Conditional Expression

The truthiness rule does not run only in the `if` statement. The same reduction runs
in a `while` condition, in the `and` and `or` operators, in the `not` operator, and in
the **conditional expression** too.

The conditional expression uses the statement-versus-expression distinction from the
previous topic: `if` is a **statement**, it produces no value and cannot be written on
the right side of an assignment. Its expression counterpart is the `a if k else b`
notation, and it **produces a value** — so it can be placed on the right side of an
assignment, in place of an argument, or inside a list.

The difference between the two is only in notation; the truthiness rule runs the same
three rungs in both, and the object written in place of `k` goes through the same
measurement. Short-circuiting is preserved too: in a conditional expression, only
**one** of the `a` and `b` branches is evaluated. This is what separates a conditional
expression from `and`/`or` chains — there, the returned value was one of the
operands; here it is the chosen branch itself, and the branch not chosen is never
built at all.

## The Type of the Returned Object

The bottom table shows what `and` and `or` return, and in all three of its rows, what
comes back is **not a logical value**.

First row: the left operand `Tracker(0)` is empty, the trial comes out false via
`__len__`, and `or` returns the right operand — the returned type is `NonEmpty`.
Second row: this time the left operand is true, tested via `__bool__`, and `or`
returns **the left operand**. Third row: `and` does the reverse: because the left
operand is false, the result is directly **it**, type `Tracker`.

All three rows' call count is **1**. The right operand is never tested; it is only
returned. The consequence of this is exactly what the default-value pattern written
with `or` does in Python: if the left operand comes out false by the truthiness rule,
the right one is given — not because it is **empty**, but because it is **counted
false**.

The last row separates the two. When the `bool()` built-in steps in, the returned type
really becomes `bool` and the call count rises to **2**: once `or` tests the left
operand, once `bool()` tests the result. The cost of turning the same expression into
a logical value is one extra protocol call.

## Summary

- In Python a condition does not expect a logical value; every object after `if` is
  put through the **truthiness rule**, and what is measured is which special method
  this reduction calls.
- The rule has three rungs: if `__bool__` exists it is called, otherwise it falls to
  `__len__`, and if that is also absent the object is counted true **without calling
  any method** — four of the twelve trials fall on this rung, and even an empty
  object comes back true.
- While `__bool__` is defined, `__len__` takes no part in the trial at all; the two
  methods do not compete, the first disables the second.
- A comparison chain `a<b<c` and the expanded notation call `__lt__` the same number
  of times (**2**), but the chain produces the middle term **once**, the expanded
  notation **twice**; when the chain comes out false, the right term is never
  produced.
- Comparison has its own fallback rule: if the left operand has no method, the right
  operand's counterpart is called — an expression written with `>` can run `__lt__`;
  if neither exists, the result is not a value but a `TypeError`.
- `and` and `or` do not return a logical value, they return **one of the operands**;
  they put only the left operand through the truthiness rule and never test the
  right one.

## Next Step

The truthiness rule asks one question once and ends with one branch. A loop, on the
other hand, asks not the same question but **another question over and over**: is
there an item next in line? In the Programming Fundamentals course a loop was a
control structure; how many turns it took depended on a condition or a collection's
length. In Python a loop is a **protocol**, and what decides the turns are ending is
not a condition. The next lesson counts how many times a loop calls `__next__` on a
three-item object — and why that count is one more than the item count.
