What does the expression `a or b` evaluate to in Python, and when is `b` skipped?
answer
- Not a boolean function
- It picks a side and returns it
- The right operand may never run
- First truthy for or, first falsy for and
- `not` is the one that returns a real bool
basics
~10 sa or b returns one of the operands, not a bool: a itself when a is truthy, otherwise b. The right operand b is evaluated only when a is falsy. and mirrors this exactly.
solid answer
~40 sPython's `or` and `and` are selectors, not boolean functions. `a or b` evaluates `a`, returns `a` unchanged if it is truthy, and only then evaluates and returns `b`. `a and b` returns `a` if `a` is falsy, otherwise `b`. So `"value" or fetch()` never calls `fetch()`, and `0 and 1/0` is `0` with no `ZeroDivisionError`. A chain of `or` yields the first truthy operand or the last one; a chain of `and` yields the first falsy operand or the last one. Short-circuiting is a semantic guarantee compiled into a jump, which is why `user is not None and user.active` is safe. `not` is the exception: it always returns a real `bool`. Neither `and` nor `or` can be overloaded — the bitwise `&` and `|` are different operators that evaluate both sides.
code
python · 7 linesdef fallback():
print("fallback() ran")
return "F"
print("value" or fallback()) # value (fallback never runs)
print("" or fallback()) # fallback() ran, then F
print(0 and fallback()) # 0 (right operand skipped)go deeper
Remember two facts: a or b gives back one of the operands rather than True or False, and the right-hand side is skipped when the left already decides the result.
Explain the selector rule for both operators and demonstrate short-circuiting with a side effect. Be able to state what a chain returns when nothing is truthy, and why not is the only one of the three that yields a real bool.
Show you treat short-circuiting as a guarantee you can build guards on, and that you notice when the operand-returning behaviour leaks a surprising type into an assignment or a function argument. Name the case where the value is discarded anyway — inside an if.
Own the readability line: guard chains and default idioms are fine, but expressions that rely on operand-returning semantics to carry a value are a place where types drift silently. Decide where the codebase prefers an explicit conditional and make static checking enforce it.
## `or` and `and` are selectors, not boolean functions In many languages `a || b` produces a boolean. In Python `a or b` produces **one of the two operands, unchanged**. The rule is short: - `a or b` → evaluate `a`; if `a` is truthy, the result *is* `a` and `b` is never evaluated. Otherwise the result is `b`, whatever `b` is. - `a and b` → evaluate `a`; if `a` is falsy, the result *is* `a` and `b` is never evaluated. Otherwise the result is `b`. So `"value" or "default"` is `"value"`, `"" or "default"` is `"default"`, `0 and 1/0` is `0` (no `ZeroDivisionError`, because the right operand never runs), and `[] or 0 or "last"` is `"last"` — a chain of `or` returns the first truthy operand, or the last operand if none is truthy. Symmetrically, a chain of `and` returns the first falsy operand, or the last operand if all are truthy. ```python def fallback(): print("fallback() ran") return "F" print("value" or fallback()) # value -- fallback() never runs print("" or fallback()) # fallback() ran, then F print(0 and fallback()) # 0 -- right operand skipped ``` ## Short-circuiting is a control-flow guarantee, not an optimisation The interpreter compiles `and`/`or` into a conditional jump over the right-hand operand, so the skipping is part of the language semantics and you may depend on it. Two everyday consequences: **Guard idioms work.** `if user is not None and user.active:` never touches an attribute of `None`, because the second operand is only reached when the first is truthy. Same for `items and items[0]`, which yields `[]` for an empty list rather than raising `IndexError`. **Side effects are ordered and conditional.** If the right operand calls a function, opens a file or increments a counter, that work happens only on the branch that reaches it. Putting a side effect on the right of an `or` is a real source of "why did this run only sometimes?" bugs — and of the opposite bug, where cleanup you assumed always ran quietly did not. Because the operators are ordinary control flow, they **cannot be overloaded**. There is no dunder method for boolean `and`/`or`; a class can only influence them through its truthiness. The bitwise operators `&` and `|` are a different thing entirely: they are ordinary binary operators, they evaluate *both* sides, and on non-boolean operands they do bitwise or set arithmetic. `[] | ["a"]` raises `TypeError` where `[] or ["a"]` returns `["a"]`. ## `not` is the odd one out `not x` does not return an operand. It converts `x` to a truth value and returns the opposite as a genuine `bool`. So in `not (a or b)` the parenthesised part may be any object, but the result of `not` is always `True` or `False`. Precedence follows the same asymmetry: `not` binds tighter than `and`, which binds tighter than `or`, and all three bind looser than comparisons — so `not a == b` means `not (a == b)`, which is a classic misreading. The builtins `any()` and `all()` also short-circuit — `any` stops at the first truthy element, `all` at the first falsy one — but unlike `or`/`and` they return a real `bool` rather than the element that decided the outcome. `any([])` is `False` and `all([])` is `True`, the vacuous-truth answer that surprises people the first time. ## Why the operand-returning behaviour matters in practice Three patterns follow directly. **The default idiom.** `name = supplied or "anonymous"` reads well and is correct *only when every falsy value should be replaced.* If `0`, `""` or `[]` are legal inputs, this silently discards them, because `or` cannot distinguish "absent" from "falsy". The explicit test `supplied if supplied is not None else "anonymous"` asks the narrower question. **Type surprises downstream.** `total = count or "n/a"` may hand a `str` to code expecting an `int`. Since the result is an operand, the expression's static type is the union of both sides, and a type checker will say so. If you want a boolean, say `bool(a or b)` — or better, write the condition where a condition belongs. **Conditions discard the value anyway.** Inside `if a or b:` the interpreter takes the truthiness of the result, so the operand-returning behaviour is invisible there. It only becomes visible the moment you *assign* the expression or pass it as an argument — which is exactly where the interesting bugs live. None of this has changed through **Python 3.14**; `and`, `or` and `not` have had these semantics for the entire Python 3 line.
- Can a class change what `and` and `or` do for its instances?Not directly. Boolean `and`/`or` compile to conditional jumps and have no dunder hook, so a class can influence them only through its own truthiness. The bitwise operators `&` and `|` are different: they dispatch to `__and__`/`__or__`, evaluate both operands, and do not short-circuit — which is why `[] | ["a"]` raises `TypeError` where `[] or ["a"]` returns `["a"]`.
- How do the builtins `any()` and `all()` differ from a chain of `or` and `and`?Both short-circuit the same way — `any` stops at the first truthy element, `all` at the first falsy one — but they return a real `bool` rather than the element that decided the outcome. They also work over any iterable rather than a fixed pair. Note the vacuous cases: `any([])` is `False` and `all([])` is `True`.
- Why does `not a == b` not mean what a reader might expect?Precedence. Comparisons bind tighter than `not`, so `not a == b` parses as `not (a == b)` — a negated equality test, not a comparison of `not a` against `b`. Among the boolean operators, `not` binds tightest, then `and`, then `or`. Parenthesise when the grouping is not obvious to a reader.
or behaves like a hiring manager reading a stack of resumes: it hands you the first acceptable one and never opens the rest, and if none are acceptable you get the last one anyway.
saying these in an interview costs you the question
- Says `a or b` always returns True or False
- Thinks both operands are always evaluated
- Believes `and`/`or` can be overloaded via dunders
- Confuses `|` with `or` and expects short-circuiting
- Claims `any()` returns the matching element
- Reads `not a == b` as `(not a) == b`