Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Explainer

Python or: Short-Circuits, Truthiness, and the Default-Value Trap

Sean

Platform Writer

Jul 16, 2026
6 min read

Python’s or returns the first operand that is truthy, or the last one if none are - and crucially, it returns the operand itself, not True or False. "a" or "b" is "a". 0 or "b" is "b". 0 or "" is "". That value-returning behaviour is what makes the popular name = name or "anonymous" idiom work, and it is also exactly why that idiom is a bug waiting for the day someone passes 0, an empty string, or an empty list - all of which are falsy, and all of which are values a user legitimately meant.

Python or: Short-Circuits, Truthiness, and the Default-Value Trap

Two things about or surprise people, and the second one causes real bugs: it does not return a boolean, and it does not evaluate its right side unless it has to.

Table of contents

It returns an operand, not a boolean

"alice" or "anonymous"    # 'alice'
"" or "anonymous"         # 'anonymous'
0 or 42                   # 42
None or []                # []      <- last operand, still falsy

[] or {} or "fallback"    # 'fallback'

The rule is exact: or evaluates operands left to right and returns the first truthy one. If all are falsy, it returns the last one - not False.

and is the mirror image: it returns the first falsy operand, or the last one if all are truthy.

"alice" and "bob"   # 'bob'    - all truthy, returns last
"" and "bob"        # ''       - first falsy
1 and 0 and 3       # 0        - first falsy, 3 never evaluated

If you need an actual boolean, wrap it: bool(x or y). Most of the time you do not, because if accepts any object and applies truthiness itself.

What counts as falsy

This list is the root of the trap in the next section, so it is worth knowing exactly:

  • False
  • None
  • 0, 0.0, Decimal(0) - every numeric zero
  • "" - the empty string
  • [], {}, (), set() - every empty container
  • range(0)
  • Any object whose __bool__ returns False, or whose __len__ returns 0

Everything else is truthy. Including "0", "False", [0], and {} with anything in it - a non-empty string is truthy no matter what it says, which catches people parsing config.

bool("False")   # True   - a non-empty string
bool("0")       # True   - also non-empty
bool([0])       # True   - a list with one element
bool(0)         # False
bool([])        # False

"False" being truthy is a classic environment-variable bug. os.environ.get("DEBUG") returns the string "False", which is truthy, and now debug mode is on in production.

The default-value trap

This is the reason to read this article. The idiom is everywhere:

def greet(name=None):
    name = name or "anonymous"
    return f"Hello, {name}"

Fine for strings where empty and missing mean the same thing. Now watch it fail:

def set_timeout(seconds=None):
    seconds = seconds or 30
    return seconds

set_timeout(0)   # 30    <- the caller explicitly said zero!

The caller asked for a zero-second timeout - a deliberate, meaningful value - and got 30. Because 0 is falsy, or skipped it. The function silently overrode an explicit instruction, and there is no error to find.

The same bug, in the shapes it usually takes:

count = user_count or 100        # 0 users becomes 100
items = selected or all_items    # deselecting everything selects everything
prefix = prefix or "/api"        # an intentional empty prefix becomes /api
retries = retries or 3           # explicitly disabling retries enables them

Every one of these is the same mistake: or tests truthiness, but you meant to test whether the value was provided. Those are different questions, and they only agree when no falsy value is legitimate.

The fix is to ask the question you meant:

def set_timeout(seconds=None):
    if seconds is None:
        seconds = 30
    return seconds

set_timeout(0)   # 0   - correct

is None tests for absence. or tests for falsiness. Use or for defaults only when you are certain that no falsy value is ever a valid input - and be aware that certainty tends to expire.

Short-circuiting is a feature you can rely on

or does not evaluate its right side if the left side is truthy. and does not evaluate its right side if the left is falsy. This is guaranteed, not an optimisation.

# Safe: the second operand never runs if user is None.
if user is not None and user.is_active:
    ...

# Safe: expensive_check() only runs if the cache misses.
result = cache.get(key) or expensive_check(key)

Because it is guaranteed, you can put a guard on the left and depend on the right never running. Reversing that order raises AttributeError on None.

The flip side: side effects on the right may never happen.

# log_attempt() is skipped entirely when the cache hits.
value = cache.get(key) or log_attempt() or fetch(key)

That is clever code and I would not ship it. If something must always run, give it its own line - hiding a side effect behind a short-circuit is how it gets deleted by someone who could not see it.

or is not the | operator

This is the top confusion in the search results for good reason - they look interchangeable and are not.

True or False    # True
True | False     # True     - same answer, different mechanism

1 or 2           # 1        - short-circuits, returns operand
1 | 2            # 3        - bitwise OR: 01 | 10 = 11

[] or [1]        # [1]
[] | [1]         # TypeError - lists have no bitwise or
  • or - logical, short-circuits, returns an operand. What you almost always want.
  • | - bitwise, evaluates both sides, calls __or__. Integers get bit arithmetic; sets get union; pandas Series get elementwise logic.

The reason | shows up in real code is libraries that overload it. In pandas, df[(df.a > 1) | (df.b > 2)] needs | because or would try to evaluate the truthiness of a whole Series and raise. In sets, {1} | {2} is union. Neither is a reason to use | for ordinary boolean logic.

Simple rule: booleans and control flow use or. If you are using |, you should be able to say why - bits, sets, or a library that overloaded it.

How this fits the rest of the stack

A default that silently overrides an explicit zero is the kind of bug that never crashes and never alerts - it just produces slightly wrong behaviour forever. Infrastructure has the same failure mode: a size that is quietly wrong costs you every month and nothing tells you. The RunxBuild hosting calculator makes the compute, database, storage, and bandwidth explicit so the total is something you checked rather than inherited, and the RunxBuild dashboard is where the team sees what is actually being used.

Useful related references:

FAQ

What does the or operator return in Python?

The first truthy operand, or the last operand if all are falsy - and it returns the operand itself, not True or False. So 0 or "b" returns the string "b", and 0 or "" returns the empty string. If you need an actual boolean, wrap the expression in bool(). Most of the time you do not, because if applies truthiness to any object.

What is the difference between or and | in Python?

or is logical, short-circuits, and returns one of its operands. | is bitwise, always evaluates both sides, and calls __or__ - so 1 | 2 is 3 rather than 1. Use or for boolean logic and control flow. | is for bit arithmetic, set union, and libraries like pandas that overload it for elementwise operations where or would raise.

Why does x or default not work with 0 in Python?

Because 0 is falsy, so or skips it and returns the default - even though the caller explicitly passed zero. The idiom tests truthiness when you meant to test whether a value was provided. Use if x is None: x = default instead. The same trap applies to empty strings, empty lists, and False, all of which are legitimate values that or discards.

What values are falsy in Python?

False, None, every numeric zero (0, 0.0), the empty string, and every empty container ([], {}, (), set()), plus any object whose __bool__ returns False or __len__ returns 0. Everything else is truthy - including the strings "0" and "False", which is why reading booleans from environment variables without parsing them is a common bug.

Does Python or short-circuit?

Yes, and it is guaranteed by the language rather than being an optimisation. or does not evaluate its right operand if the left is truthy, and and does not if the left is falsy. This lets you write if user is not None and user.is_active safely. It also means side effects on the right side may never run, so anything that must always happen belongs on its own line.

#python or#python#operators#boolean logic#dev-infra