Short-circuit Evaluation

Concepts covered: Boolean Operators

and and or evaluate their left operand first and stop the moment the answer is settled. If the left side of and is false, the whole expression is false whatever the right side holds, so Python returns the left operand and never evaluates the right. If the left side of or is true, the same thing happens in mirror image. A skipped operand does nothing at all, so a function call inside it never runs and a division inside it never gets the chance to raise an error.

print(True or print("right side ran"))
print(False and print("right side ran"))
print(False or print("right side ran"))

Output:

True
False
right side ran
None

print returns None, which makes it a witness: its message appears only when Python reaches the right operand. On the third line Python does reach it, and the value of the whole or is whatever that right operand returned, None.

ExpressionLeft operand isRight operand evaluated?Result
`a and b`falseno`a`
`a and b`trueyes`b`
`a or b`trueno`a`
`a or b`falseyes`b`

Read the Result column closely: and and or hand back an operand, unchanged. True and 7 is 7, and None or False is False. The next section pins down exactly which values count as false; for now bools and None are enough.

Put the guard on the left

Because the right operand runs only when needed, a cheap test on the left can protect a risky one on the right. Dividing by a row count is safe when row_count != 0 comes first. Reversing the operands evaluates the division before the test gets a chance to stop it.

total_bytes = 5000
row_count = 0
print(row_count != 0 and total_bytes / row_count > 100)
try:
    print(total_bytes / row_count > 100 and row_count != 0)
except ZeroDivisionError as err:
    print("ZeroDivisionError:", err)

Output:

False
ZeroDivisionError: division by zero

The same ordering protects against None. In latency is not None and latency > 500, a missing reading stops at the left test; the reversed form evaluates None > 500 and raises TypeError. Ordering also saves work: when 99 rows in 100 fail a quick comparison on the left, an expensive conversion on the right runs for only 1 of them.

Chains stop at the deciding operand

print(None or False or "eu-west" or "us-east")
print(True and 3 and None and print("never"))

Output:

eu-west
None

The first chain finds its first true operand in "eu-west" and returns it; "us-east" is never examined. The second runs into None as its first false operand and returns it, so the print at the end never fires and the word never appears.

Defaults with or, and their trap

label = configured_label or "unlabeled" keeps the configured value when it is set and falls back when it is None. The fallback also fires for every other false value. A retry setting of 0 means 'never retry', yet configured_retries or 3 replaces it with 3, because 0 counts as false. When a false value is a legitimate setting, test for None with is, since or cannot tell 0 from a missing value.

configured_label = None
configured_retries = 0
print(configured_label or "unlabeled")
print(configured_retries or 3)
print(configured_retries is None)

Output:

unlabeled
3
False

The third line keeps the setting intact: configured_retries is None is False, because 0 is a value and not a missing one. Defaults also chain. cli_region or env_region or "us-east" returns the first setting that is filled in, which lets a job prefer a command-line flag, then an environment value, then a hard-coded fallback, all in one expression. Every link in that chain has the retry setting's problem: a link holding 0 or an empty string is skipped as if it were unset.

What does this program print?

count = 0
print(count == 0 or 10 / count > 1)
print(count > 0 and int("abc") > 1)

About this lesson

This section is part of the Python Expressions: Advanced lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.