Python Short-Circuit Evaluation Practice Problems & Exercises
Practice: Short-Circuit Evaluation
← Back to lessonEasy
Predict whether the right side gets evaluated in each expression. We use a helper function that prints when called, so you can see exactly what Python evaluates.
def probe(label, value):
"""Returns value and prints that it was called."""
print(f" [called: {label}]")
return value
results = []
# Test 1: False and ... (left is falsy)
print("Test 1: and stops at first falsy")
result = 0 and probe("right", 42)
evaluated = "right side NOT evaluated"
print(f" -> {evaluated}")
# Test 2: True and ... (left is truthy)
print("Test 2: and with all truthy")
result = 1 and probe("right", 42)
evaluated = "right side evaluated"
print(f" -> {evaluated}")
# Test 3: True or ... (left is truthy)
print("Test 3: or stops at first truthy")
result = "hello" or probe("right", 99)
evaluated = "right side NOT evaluated"
print(f" -> {evaluated}")
# Test 4: False or ... (left is falsy)
print("Test 4: or with all falsy")
result = "" or probe("right", 99)
evaluated = "right side evaluated"
print(f" -> {evaluated}")
# Test 5: [] and ...
print("Test 5: and with falsy left")
result = [] and probe("right", "data")
evaluated = "right side NOT evaluated"
print(f" -> {evaluated}")
# Test 6: [1] or ...
print("Test 6: or with truthy left")
result = [1] or probe("right", "data")
evaluated = "right side NOT evaluated"
print(f" -> {evaluated}")Solution
Test 1: and stops at first falsy -> right side NOT evaluated
Test 2: and with all truthy -> right side evaluated
Test 3: or stops at first truthy -> right side NOT evaluated
Test 4: or with all falsy -> right side evaluated
Test 5: and with falsy left -> right side NOT evaluated
Test 6: or with truthy left -> right side NOT evaluated
The two short-circuit rules:
andskips the right side when the left is falsy. If the left operand is falsy, the wholeandexpression is guaranteed to be falsy, so Python returns the left value immediately.orskips the right side when the left is truthy. If the left operand is truthy, the wholeorexpression is guaranteed to be truthy, so Python returns the left value immediately.
Notice that probe() only prints [called: right] in Tests 2 and 4 — those are the only cases where Python actually evaluates the right-hand side.
Expected Output
Test 1: and stops at first falsy -> right side NOT evaluated
Test 2: and with all truthy -> right side evaluated
Test 3: or stops at first truthy -> right side NOT evaluated
Test 4: or with all falsy -> right side evaluated
Test 5: and with falsy left -> right side NOT evaluated
Test 6: or with truthy left -> right side NOT evaluatedHints
Hint 1: `and` stops as soon as it finds a falsy value — it does not need to check further because False AND anything is always False.
Hint 2: `or` stops as soon as it finds a truthy value — it does not need to check further because True OR anything is always True.
Predict the return value of each or chain. Remember: or returns the first truthy value, or the last value if all are falsy.
# Chain 1: Mixed falsy and truthy
r1 = 0 or "" or None or 42 or 99
print(f'Chain 1: 0 or "" or None or 42 or 99 -> {r1}')
# Chain 2: ALL falsy — returns last
r2 = False or 0 or "" or [] or {}
print(f'Chain 2: False or 0 or "" or [] or {{}} -> {r2}')
# Chain 3: First truthy is "backup"
r3 = "" or "backup" or "fallback"
print(f'Chain 3: "" or "backup" or "fallback" -> {r3}')
# Chain 4: Long falsy chain then truthy at end
r4 = None or 0 or "" or 0.0 or "end"
print(f'Chain 4: None or 0 or "" or 0.0 or "end" -> {r4}')
# Chain 5: Short-circuit prevents error
r5 = "first" or 1/0
print(f'Chain 5: "first" or 1/0 -> {r5}')Solution
Chain 1: 0 or "" or None or 42 or 99 -> 42
Chain 2: False or 0 or "" or [] or {} -> {}
Chain 3: "" or "backup" or "fallback" -> backup
Chain 4: None or 0 or "" or 0.0 or "end" -> end
Chain 5: "first" or 1/0 -> first
How or chains work:
- Chain 1:
0falsy,""falsy,Nonefalsy,42truthy — returns42. The99is never evaluated. - Chain 2: Every value is falsy. When all values are falsy,
orreturns the last value:{}. - Chain 3:
""falsy,"backup"truthy — returns"backup"."fallback"is never evaluated. - Chain 4:
None,0,"",0.0all falsy,"end"truthy — returns"end". - Chain 5:
"first"is truthy — returns immediately.1/0is never executed, so noZeroDivisionError. This is short-circuit evaluation in action.
Expected Output
Chain 1: 0 or "" or None or 42 or 99 -> 42
Chain 2: False or 0 or "" or [] or {} -> {}
Chain 3: "" or "backup" or "fallback" -> backup
Chain 4: None or 0 or "" or 0.0 or "end" -> end
Chain 5: "first" or 1/0 -> firstHints
Hint 1: `or` returns the FIRST truthy value it encounters, scanning left to right. If no value is truthy, it returns the LAST value.
Hint 2: In Chain 5, `"first"` is truthy so `or` returns it immediately — `1/0` is never evaluated, so no ZeroDivisionError.
Implement two functions using the value or default pattern, then identify the gotcha where it breaks.
def get_greeting(name):
"""Return a greeting, using 'stranger' if name is empty/None."""
display_name = name or "stranger"
return f"Hello, {display_name}!"
def get_config(port, user_port):
"""Return a port number, using 3000 as default."""
actual_port = user_port or port or 3000
return actual_port
# Normal cases — works perfectly
print(f'get_greeting("Alice") -> {get_greeting("Alice")}')
print(f'get_greeting("") -> {get_greeting("")}')
print(f'get_greeting(None) -> {get_greeting(None)}')
print(f'get_config("port", 8080) -> port = {get_config("port", 8080)}')
print(f'get_config("port", None) -> port = {get_config("port", None)}')
print(f'get_config("", 5000) -> port = {get_config("", 5000)}')
# GOTCHA: 0 and False are falsy!
print(f'get_config(0, 9999) -> port = {get_config(0, 9999)} (GOTCHA!)')
print(f'get_config(False, 7777) -> port = {get_config(False, 7777)} (GOTCHA!)')Solution
get_greeting("Alice") -> Hello, Alice!
get_greeting("") -> Hello, stranger!
get_greeting(None) -> Hello, stranger!
get_config("port", 8080) -> port = 8080
get_config("port", None) -> port = 3000
get_config("", 5000) -> port = 5000
get_config(0, 9999) -> port = 9999 (GOTCHA!)
get_config(False, 7777) -> port = 7777 (GOTCHA!)
The or default pattern:
value or default is a clean Python idiom for providing fallback values. It works great when:
- You want to replace
None,"",[],{}with a default - You know the valid values are always truthy
The gotcha:
The pattern breaks when 0, False, or other falsy values are valid inputs. 0 or 3000 returns 3000 because 0 is falsy — even though 0 might be a perfectly valid port number in your application.
The fix — use explicit None checks when 0/False are valid:
actual_port = port if port is not None else 3000
Or in Python 3.8+, use the walrus operator or simply an if statement.
Expected Output
get_greeting("Alice") -> Hello, Alice!
get_greeting("") -> Hello, stranger!
get_greeting(None) -> Hello, stranger!
get_config("port", 8080) -> port = 8080
get_config("port", None) -> port = 3000
get_config("", 5000) -> port = 5000
get_config(0, 9999) -> port = 9999 (GOTCHA!)
get_config(False, 7777) -> port = 7777 (GOTCHA!)Hints
Hint 1: The `value or default` pattern returns `default` whenever `value` is falsy — not just when it is None or empty string.
Hint 2: This pattern has a gotcha: `0 or default` returns `default` because `0` is falsy. Same for `False or default`. If 0 or False are valid values, use `value if value is not None else default` instead.
Medium
Track which functions are called in each short-circuit scenario. Each function logs its name to a shared list when called.
call_log = []
def make_func(name, return_value):
"""Create a function that logs when called and returns a value."""
def func():
call_log.append(name)
print(f"Called: {name}")
return return_value
return func
# Scenario 1: True and f() and g()
call_log = []
alpha = make_func("alpha", "alpha_result")
beta = make_func("beta", "beta_result")
print("--- Scenario 1: True and f() and g() ---")
result = 1 and alpha() and beta()
print(f"Result: {result}")
print(f"Functions called: {call_log}")
# Scenario 2: False and f() and g()
call_log = []
alpha = make_func("alpha", "alpha_result")
beta = make_func("beta", "beta_result")
print("--- Scenario 2: False and f() and g() ---")
result = 0 and alpha() and beta()
print(f"Result: {result}")
print(f"Functions called: {call_log}")
# Scenario 3: or chain where first function returns truthy
call_log = []
alpha = make_func("alpha", "alpha_result")
beta = make_func("beta", "beta_result")
gamma = make_func("gamma", "gamma_result")
print("--- Scenario 3: f() or g() or h() (f returns truthy) ---")
result = alpha() or beta() or gamma()
print(f"Result: {result}")
print(f"Functions called: {call_log}")
# Scenario 4: or chain where all functions return falsy
call_log = []
f1 = make_func("returns_empty", "")
f2 = make_func("returns_zero", 0)
f3 = make_func("returns_none", None)
print("--- Scenario 4: f() or g() or h() (all return falsy) ---")
result = f1() or f2() or f3()
print(f"Result: {result}")
print(f"Functions called: {call_log}")Solution
--- Scenario 1: True and f() and g() ---
Called: alpha
Called: beta
Result: beta_result
Functions called: ['alpha', 'beta']
--- Scenario 2: False and f() and g() ---
Result: 0
Functions called: []
--- Scenario 3: f() or g() or h() (f returns truthy) ---
Called: alpha
Result: alpha_result
Functions called: ['alpha']
--- Scenario 4: f() or g() or h() (all return falsy) ---
Called: returns_empty
Called: returns_zero
Called: returns_none
Result: None
Functions called: ['returns_empty', 'returns_zero', 'returns_none']
Why this matters:
Short-circuit evaluation is not just about return values — it controls which side effects happen. This is critical in real code:
- Scenario 1:
1is truthy, soandcontinues.alpha()returns truthy, soandcontinues.beta()is the last value — returned. Both functions called. - Scenario 2:
0is falsy.andreturns0immediately. Neitheralpha()norbeta()is ever called. Their side effects (logging, database writes, API calls) never happen. - Scenario 3:
alpha()returns a truthy string.orreturns it immediately.beta()andgamma()are never called. - Scenario 4: Every function returns a falsy value, so
ormust evaluate all of them before returning the last one (None).
Real-world impact: If you write should_send and send_email(), the email only sends when should_send is truthy. This is a deliberate pattern, not a bug.
Expected Output
--- Scenario 1: True and f() and g() ---
Called: alpha
Called: beta
Result: beta_result
Functions called: ['alpha', 'beta']
--- Scenario 2: False and f() and g() ---
Result: 0
Functions called: []
--- Scenario 3: f() or g() or h() (f returns truthy) ---
Called: alpha
Result: alpha_result
Functions called: ['alpha']
--- Scenario 4: f() or g() or h() (all return falsy) ---
Called: returns_empty
Called: returns_zero
Called: returns_none
Result: None
Functions called: ['returns_empty', 'returns_zero', 'returns_none']Hints
Hint 1: Track which functions get called by appending to a shared list inside each function. The call log reveals exactly what Python evaluated.
Hint 2: In `and` chains, evaluation stops at the first falsy return. In `or` chains, evaluation stops at the first truthy return.
Implement safe nested attribute access using and chains to avoid AttributeError, without using try/except or getattr.
class Address:
def __init__(self, city, state=None, zip_code=None):
self.city = city
self.state = state
self.zip_code = zip_code
class User:
def __init__(self, name, address=None):
self.name = name
self.address = address
def get_city(user):
"""Safely get user's city using and-chain, return 'Unknown' if any part is None."""
return (user and user.address and user.address.city) or "Unknown"
def get_city_and_zip(user):
"""Safely get city and zip using and-chains with or-defaults."""
city = (user and user.address and user.address.city) or "Unknown"
zip_code = (user and user.address and user.address.zip_code) or "Unknown"
return city, zip_code
# Test cases
alice = User("alice", Address("Springfield", "IL", "62704"))
bob = User("bob", Address(None)) # address exists but city is None
charlie = User("charlie", None) # no address at all
print(f"User 'alice': city = {get_city(alice)}")
print(f"User 'bob': city = {get_city(bob)}")
print(f"User 'charlie': city = {get_city(charlie)}")
print(f"User None: city = {get_city(None)}")
print("---")
valid = User("valid", Address("Springfield", "IL", "62704"))
no_zip = User("no_zip", Address("Springfield", "IL", None))
no_addr = User("no_addr", None)
city1, zip1 = get_city_and_zip(valid)
print(f"Deep access 'valid': {city1}, {zip1}")
city2, zip2 = get_city_and_zip(no_zip)
print(f"Deep access 'no_zip': {city2}, {zip2}")
city3, zip3 = get_city_and_zip(no_addr)
print(f"Deep access 'no_addr': {city3}, {zip3}")
city4, zip4 = get_city_and_zip(None)
print(f"Deep access 'none': {city4}, {zip4}")Solution
User 'alice': city = Springfield
User 'bob': city = Unknown
User 'charlie': city = Unknown
User None: city = Unknown
---
Deep access 'valid': Springfield, IL
Deep access 'no_zip': Springfield, Unknown
Deep access 'no_addr': Unknown, Unknown
Deep access 'none': Unknown, Unknown
How and-chain safe access works:
The expression user and user.address and user.address.city evaluates left to right:
user— ifNone(falsy), returnsNoneimmediately. Never touches.address.user.address— ifNone(falsy), returnsNone. Never touches.city.user.address.city— only reached if bothuseranduser.addressare truthy.
Then (...) or "Unknown" provides the default when the chain returns a falsy value.
Why each test case works:
- alice:
Usertruthy,Addresstruthy,city = "Springfield"truthy — returns"Springfield". - bob:
Usertruthy,Address(None)truthy (object exists),city = Nonefalsy —andreturnsNone, thenor "Unknown"kicks in. - charlie:
Usertruthy,address = Nonefalsy —andreturnsNone,or "Unknown"kicks in. - None:
Noneis falsy —andreturnsNoneimmediately,or "Unknown"kicks in.
Modern Python alternative: Python 3.10+ has structural pattern matching, and many codebases use getattr(obj, 'attr', default) chains. But the and/or pattern remains the most concise for simple cases.
Expected Output
User 'alice': city = Springfield
User 'bob': city = Unknown
User 'charlie': city = Unknown
User None: city = Unknown
---
Deep access 'valid': Springfield, IL
Deep access 'no_zip': Springfield, Unknown
Deep access 'no_addr': Unknown, Unknown
Deep access 'none': Unknown, UnknownHints
Hint 1: The pattern `obj and obj.attr and obj.attr.nested` short-circuits at the first falsy (None) value, preventing AttributeError on subsequent access.
Hint 2: Combine `and` for safe traversal with `or` for default values: `(obj and obj.name) or "default"`.
Implement my_any() and my_all() that behave identically to Python builtins, including short-circuit behavior. Track how many items are checked.
def my_any(iterable):
"""Return True if any element is truthy. Short-circuit on first truthy."""
count = 0
for item in iterable:
count += 1
if item:
return True, count
return False, count
def my_all(iterable):
"""Return True if all elements are truthy. Short-circuit on first falsy."""
count = 0
for item in iterable:
count += 1
if not item:
return False, count
return True, count
# Test my_any
print("--- Testing my_any ---")
test_cases_any = [
([0, "", None, 42, 99], "my_any([0, \"\", None, 42, 99])"),
([0, "", None, [], {}], "my_any([0, \"\", None, [], {}])"),
([], "my_any([])"),
([1], "my_any([1])"),
]
for data, label in test_cases_any:
result, checked = my_any(data)
print(f"{label + ':':40s}{str(result):6s}(checked {checked} items)")
# Test my_all
print("--- Testing my_all ---")
test_cases_all = [
([1, "yes", True, [1]], 'my_all([1, "yes", True, [1]])'),
([1, "yes", 0, [1]], 'my_all([1, "yes", 0, [1]])'),
([], "my_all([])"),
([0], "my_all([0])"),
]
for data, label in test_cases_all:
result, checked = my_all(data)
print(f"{label + ':':40s}{str(result):6s}(checked {checked} items)")
# Verify against builtins
print("--- Verify against builtins ---")
test_lists = [[0, 1, 2], [0, "", None], [1, 2, 3], [], [0]]
any_match = all(my_any(t)[0] == any(t) for t in test_lists)
all_match = all(my_all(t)[0] == all(t) for t in test_lists)
print(f"any matches: {any_match}")
print(f"all matches: {all_match}")Solution
--- Testing my_any ---
my_any([0, "", None, 42, 99]): True (checked 4 items)
my_any([0, "", None, [], {}]): False (checked 5 items)
my_any([]): False (checked 0 items)
my_any([1]): True (checked 1 items)
--- Testing my_all ---
my_all([1, "yes", True, [1]]): True (checked 4 items)
my_all([1, "yes", 0, [1]]): False (checked 3 items)
my_all([]): True (checked 0 items)
my_all([0]): False (checked 1 items)
--- Verify against builtins ---
any matches: True
all matches: True
Key insights:
my_anyshort-circuits on first truthy: In the first test, it skips0,"",None(all falsy), hits42(truthy), and returnsTrueafter checking only 4 of 5 items. It never sees99.my_allshort-circuits on first falsy: In the second test, it passes1,"yes"(both truthy), hits0(falsy), and returnsFalseafter checking only 3 of 4 items. It never sees[1].- Empty iterables:
any([])returnsFalse(no truthy element found) andall([])returnsTrue(vacuous truth — no falsy element found to disprove it).
This is exactly how the CPython builtins work. The real any() and all() are implemented in C but follow the same short-circuit logic. This is why any(expensive_check(x) for x in huge_list) is efficient — it stops at the first truthy result.
Expected Output
--- Testing my_any ---
my_any([0, "", None, 42, 99]): True (checked 4 items)
my_any([0, "", None, [], {}]): False (checked 5 items)
my_any([]): False (checked 0 items)
my_any([1]): True (checked 1 items)
--- Testing my_all ---
my_all([1, "yes", True, [1]]): True (checked 4 items)
my_all([1, "yes", 0, [1]]): False (checked 3 items)
my_all([]): True (checked 0 items)
my_all([0]): False (checked 1 items)
--- Verify against builtins ---
any matches: True
all matches: TrueHints
Hint 1: `any()` should return True as soon as it finds the first truthy element — do not continue iterating. This is the short-circuit behavior.
Hint 2: `all()` should return False as soon as it finds the first falsy element. An empty iterable returns True for `all()` (vacuous truth) and False for `any()`.
Build a validation chain where each validator is connected with and. The chain should stop at the first failure — later validators should not execute.
def make_validator(name, check_fn):
"""Create a validator that prints when run and returns True/False."""
def validator(data):
print(f"Checking: {name}...", end=" ")
if check_fn(data):
print("OK")
return True
else:
print("FAIL")
return False
return validator, name
def validate(data, validators):
"""Run validators connected by and-logic. Stop at first failure."""
failed_at = None
result = True
for validator_fn, name in validators:
if not validator_fn(data):
failed_at = name
result = False
break
return result, failed_at
# Define validators
validators = [
make_validator("not empty", lambda d: len(d) > 0),
make_validator("has username", lambda d: bool(d.get("username"))),
make_validator("has email", lambda d: bool(d.get("email"))),
make_validator("valid age", lambda d: isinstance(d.get("age"), int) and d["age"] > 0),
]
# Test cases
test_data = [
{"username": "alice", "email": "[email protected]", "age": 25},
{"username": "", "email": "[email protected]", "age": 30},
{},
{"username": "dave", "email": "[email protected]", "age": -5},
]
for data in test_data:
print(f"--- Validating: {data} ---")
is_valid, failed = validate(data, validators)
if is_valid:
print("Result: VALID")
else:
print(f"Result: INVALID - {failed}")Solution
--- Validating: {'username': 'alice', 'email': '[email protected]', 'age': 25} ---
Checking: not empty... OK
Checking: has username... OK
Checking: has email... OK
Checking: valid age... OK
Result: VALID
--- Validating: {'username': '', 'email': '[email protected]', 'age': 30} ---
Checking: not empty... OK
Checking: has username... FAIL
Result: INVALID - has username
--- Validating: {} ---
Checking: not empty... FAIL
Result: INVALID - not empty
--- Validating: {'username': 'dave', 'email': '[email protected]', 'age': -5} ---
Checking: not empty... OK
Checking: has username... OK
Checking: has email... OK
Checking: valid age... FAIL
Result: INVALID - valid age
Why short-circuit validation matters:
The break in our loop mimics and short-circuit behavior: once a validator returns False, we stop running subsequent validators. This is the same pattern used in production validation frameworks.
Look at the print output to confirm short-circuiting:
- alice: All 4 validators run — all pass.
- bob: Only 2 validators run.
has usernamefails (empty string is falsy), sohas emailandvalid ageare never checked. - empty dict: Only 1 validator runs.
not emptyfails immediately. - dave: 4 validators run. The first 3 pass, but
valid agefails on-5.
Production pattern: This is how Django form validators, Pydantic validators, and API input validation work — fail fast, return the first error, skip unnecessary computation.
Expected Output
--- Validating: {'username': 'alice', 'email': '[email protected]', 'age': 25} ---
Checking: not empty... OK
Checking: has username... OK
Checking: has email... OK
Checking: valid age... OK
Result: VALID
--- Validating: {'username': '', 'email': '[email protected]', 'age': 30} ---
Checking: not empty... OK
Checking: has username... FAIL
Result: INVALID - has username
--- Validating: {} ---
Checking: not empty... FAIL
Result: INVALID - not empty
--- Validating: {'username': 'dave', 'email': '[email protected]', 'age': -5} ---
Checking: not empty... OK
Checking: has username... OK
Checking: has email... OK
Checking: valid age... FAIL
Result: INVALID - valid ageHints
Hint 1: Build a chain of validator functions connected with `and`. Since `and` short-circuits on the first falsy result, subsequent validators are never called after a failure.
Hint 2: Each validator should return a truthy value on success and a falsy value on failure. Use the side effect (printing) to prove which validators ran.
Hard
Build a lazy evaluation pipeline where each stage is a generator. Elements flow through stages one at a time (not batch), and early termination skips unneeded work.
class LazyPipeline:
def __init__(self, data):
self._source = iter(data)
self._stages = []
def map(self, func, name="map"):
"""Add a transformation stage."""
self._stages.append(("map", func, name))
return self
def filter(self, predicate, name="filter"):
"""Add a filter stage."""
self._stages.append(("filter", predicate, name))
return self
def _build_chain(self):
"""Build the generator chain from stages."""
source = self._source
for stage_type, func, name in self._stages:
if stage_type == "map":
source = self._map_stage(source, func, name)
elif stage_type == "filter":
source = self._filter_stage(source, func, name)
return source
def _map_stage(self, source, func, name):
for item in source:
print(f"Stage '{name}': processing {item}")
yield func(item)
def _filter_stage(self, source, predicate, name):
for item in source:
print(f"Stage '{name}': processing {item}")
if predicate(item):
yield item
def collect(self):
"""Consume the entire pipeline and return a list."""
return list(self._build_chain())
def take(self, n):
"""Consume only n items — short-circuits the rest."""
result = []
chain = self._build_chain()
for item in chain:
result.append(item)
if len(result) >= n:
break
return result
# Pipeline 1: Transform and filter
print("--- Pipeline 1: Transform and filter ---")
result1 = (LazyPipeline([1, 2, 3, 4, 5])
.map(lambda x: x * 2, "double")
.filter(lambda x: x > 2, "add_10")
.map(lambda x: x + 10, "add_10")
.collect())
print(f"Result: {result1}")
# Pipeline 2: Early termination
print("--- Pipeline 2: Early termination with take ---")
result2 = (LazyPipeline([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
.map(lambda x: x * x, "square")
.take(3))
print(f"First 3 squares: {result2}")
# Pipeline 3: Filter then transform
print("--- Pipeline 3: Filter then transform (lazy) ---")
result3 = (LazyPipeline([1, 2, 3, 4, 5, 6])
.filter(lambda x: x % 2 == 0, "is_even")
.map(lambda x: x * 3, "triple")
.collect())
print(f"Filtered + transformed: {result3}")Solution
--- Pipeline 1: Transform and filter ---
Stage 'double': processing 1
Stage 'double': processing 2
Stage 'add_10': processing 2
Stage 'double': processing 3
Stage 'add_10': processing 4
Stage 'double': processing 4
Stage 'add_10': processing 6
Stage 'double': processing 5
Stage 'add_10': processing 8
Stage 'add_10': processing 10
Result: [12, 14, 16, 18, 20]
--- Pipeline 2: Early termination with take ---
Stage 'square': processing 1
Stage 'square': processing 2
Stage 'square': processing 3
First 3 squares: [1, 4, 9]
--- Pipeline 3: Filter then transform (lazy) ---
Stage 'is_even': processing 1
Stage 'is_even': processing 2
Stage 'triple': processing 2
Stage 'is_even': processing 3
Stage 'is_even': processing 4
Stage 'triple': processing 4
Stage 'is_even': processing 5
Stage 'is_even': processing 6
Stage 'triple': processing 6
Filtered + transformed: [6, 12, 18]
How lazy evaluation connects to short-circuit semantics:
This pipeline demonstrates the same principle behind and/or short-circuiting, scaled to data processing:
-
Pipeline 2 (
take(3)): The source has 10 elements, but only 3 are ever processed. Thebreakintake()stops pulling from the generator chain — elements 4 through 10 are never touched. This is short-circuit evaluation applied to iteration. -
Pipeline 3 (filter + map): Watch the interleaved output. Element
1entersis_even, fails the filter, and is discarded — it never reachestriple. Element2passes the filter and flows totriple. This is element-at-a-time lazy processing, not batch-then-filter.
Why generators enable this: Each yield suspends the function. The downstream consumer pulls one value at a time. When the consumer stops asking (via break or exhaustion), upstream generators simply never resume — no wasted computation.
Real-world usage: This pattern powers Apache Spark transformations, Python's itertools module, Rust iterators, and Java Streams.
Expected Output
--- Pipeline 1: Transform and filter ---
Stage 'double': processing 1
Stage 'double': processing 2
Stage 'add_10': processing 2
Stage 'double': processing 3
Stage 'add_10': processing 4
Stage 'double': processing 4
Stage 'add_10': processing 6
Stage 'double': processing 5
Stage 'add_10': processing 8
Stage 'add_10': processing 10
Result: [12, 14, 16, 18, 20]
--- Pipeline 2: Early termination with take ---
Stage 'square': processing 1
Stage 'square': processing 2
Stage 'square': processing 3
First 3 squares: [1, 4, 9]
--- Pipeline 3: Filter then transform (lazy) ---
Stage 'is_even': processing 1
Stage 'is_even': processing 2
Stage 'triple': processing 2
Stage 'is_even': processing 3
Stage 'is_even': processing 4
Stage 'triple': processing 4
Stage 'is_even': processing 5
Stage 'is_even': processing 6
Stage 'triple': processing 6
Filtered + transformed: [6, 12, 18]Hints
Hint 1: Use generators (yield) to make each pipeline stage lazy — it only processes an element when the next stage requests it.
Hint 2: For `take(n)`, consume only `n` items from the generator and stop — this is where short-circuit semantics meet lazy evaluation.
Build a circuit breaker that uses short-circuit evaluation to prevent calls when the circuit is open. The breaker tracks failures and transitions between CLOSED, OPEN, and HALF_OPEN states.
import time
class CircuitBreaker:
def __init__(self, failure_threshold=3, cooldown_seconds=0.1):
self.failure_threshold = failure_threshold
self.cooldown_seconds = cooldown_seconds
self.failure_count = 0
self.state = "CLOSED"
self.last_failure_time = 0
self.state_history = [self.state]
def _is_circuit_allowing(self):
"""Check if circuit allows a call through."""
if self.state == "CLOSED":
return True
if self.state == "OPEN":
# Check if cooldown has elapsed
elapsed = time.time() - self.last_failure_time
if elapsed >= self.cooldown_seconds:
self._transition("HALF_OPEN")
return True
return False
if self.state == "HALF_OPEN":
return True
return False
def _transition(self, new_state):
if self.state != new_state:
self.state = new_state
self.state_history.append(new_state)
def _on_success(self):
self.failure_count = 0
self._transition("CLOSED")
def _on_failure(self, error):
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self._transition("OPEN")
def call(self, func, *args, **kwargs):
"""Execute func only if circuit allows — short-circuit if open."""
# Short-circuit pattern: check circuit FIRST
allowed = self._is_circuit_allowing()
if not allowed:
return False, "BLOCKED (circuit open)"
# Circuit allows — execute the actual call
try:
result = func(*args, **kwargs)
self._on_success()
return True, f"success (result={result})"
except Exception as e:
self._on_failure(e)
return False, f"failure ({e})"
# Simulate a flaky service
call_count = 0
def flaky_service(should_fail=False):
global call_count
call_count += 1
if should_fail:
raise ConnectionError("Connection refused")
return "OK"
# Demo
breaker = CircuitBreaker(failure_threshold=3, cooldown_seconds=0.1)
print("--- Normal operation ---")
for i in range(1, 3):
ok, msg = breaker.call(flaky_service, should_fail=False)
print(f"Call {i}: attempt_call -> {msg}")
print("--- Simulating failures ---")
for i in range(3, 6):
ok, msg = breaker.call(flaky_service, should_fail=True)
print(f"Call {i}: attempt_call -> {msg}")
print("--- Circuit OPEN — calls blocked ---")
for i in range(6, 8):
ok, msg = breaker.call(flaky_service, should_fail=False)
print(f"Call {i}: attempt_call -> {msg}")
print("--- After cooldown, circuit half-open ---")
time.sleep(0.15) # Wait for cooldown
ok, msg = breaker.call(flaky_service, should_fail=False)
print(f"Call 8: attempt_call -> {msg}")
print("--- Circuit CLOSED again ---")
ok, msg = breaker.call(flaky_service, should_fail=False)
print(f"Call 9: attempt_call -> {msg}")
print(f"State transitions: {' -> '.join(breaker.state_history)}")Solution
--- Normal operation ---
Call 1: attempt_call -> success (result=OK)
Call 2: attempt_call -> success (result=OK)
--- Simulating failures ---
Call 3: attempt_call -> failure (Connection refused)
Call 4: attempt_call -> failure (Connection refused)
Call 5: attempt_call -> failure (Connection refused)
--- Circuit OPEN — calls blocked ---
Call 6: attempt_call -> BLOCKED (circuit open)
Call 7: attempt_call -> BLOCKED (circuit open)
--- After cooldown, circuit half-open ---
Call 8: attempt_call -> success (result=OK)
--- Circuit CLOSED again ---
Call 9: attempt_call -> success (result=OK)
State transitions: CLOSED -> OPEN -> HALF_OPEN -> CLOSED
Short-circuit evaluation as a resilience pattern:
The circuit breaker embodies short-circuit semantics at the architecture level:
- CLOSED state:
_is_circuit_allowing()returnsTrue, so the actual function executes. This is likeTrue and func()— the right side runs. - OPEN state:
_is_circuit_allowing()returnsFalse, so we return "BLOCKED" immediately. The actual function never executes. This is likeFalse and func()— short-circuited. - HALF_OPEN state: After cooldown, one test call is allowed through. If it succeeds, the circuit closes. If it fails, it reopens.
The state machine:
success failure >= threshold
CLOSED -------> CLOSED (reset count)
| |
| OPEN <-+
| |
| cooldown elapsed
| |
+--- HALF_OPEN
| |
success failure
| |
CLOSED OPEN
Real-world usage: Netflix Hystrix, Python's pybreaker, resilience4j — all implement this pattern. The short-circuit check (is_circuit_allowing() and actual_call()) prevents cascading failures by fast-failing when a downstream service is known to be unhealthy.
Expected Output
--- Normal operation ---
Call 1: attempt_call -> success (result=OK)
Call 2: attempt_call -> success (result=OK)
--- Simulating failures ---
Call 3: attempt_call -> failure (Connection refused)
Call 4: attempt_call -> failure (Connection refused)
Call 5: attempt_call -> failure (Connection refused)
--- Circuit OPEN — calls blocked ---
Call 6: attempt_call -> BLOCKED (circuit open)
Call 7: attempt_call -> BLOCKED (circuit open)
--- After cooldown, circuit half-open ---
Call 8: attempt_call -> success (result=OK)
--- Circuit CLOSED again ---
Call 9: attempt_call -> success (result=OK)
State transitions: CLOSED -> OPEN -> HALF_OPEN -> CLOSEDHints
Hint 1: The circuit breaker has three states: CLOSED (normal), OPEN (blocking calls), HALF_OPEN (testing). Use short-circuit `and`/`or` to gate whether the actual function call executes.
Hint 2: The key pattern is: `circuit_allows() and actual_call()` — if the circuit is open, `circuit_allows()` returns False and `actual_call()` is never executed (short-circuited).
Build a composable rule engine where rules combine with AND, OR, NOT and short-circuit during evaluation, just like Python operators.
class Rule:
"""Base class for composable, short-circuit rules."""
def evaluate(self, context):
raise NotImplementedError
def AND(self, other):
return AndRule(self, other)
def OR(self, other):
return OrRule(self, other)
def NOT(self):
return NotRule(self)
class PredicateRule(Rule):
def __init__(self, name, predicate):
self.name = name
self.predicate = predicate
def evaluate(self, context):
context["_checks"] = context.get("_checks", 0) + 1
return self.predicate(context)
class AndRule(Rule):
def __init__(self, left, right):
self.left = left
self.right = right
def evaluate(self, context):
# Short-circuit: if left is False, skip right
if not self.left.evaluate(context):
return False
return self.right.evaluate(context)
class OrRule(Rule):
def __init__(self, left, right):
self.left = left
self.right = right
def evaluate(self, context):
# Short-circuit: if left is True, skip right
if self.left.evaluate(context):
return True
return self.right.evaluate(context)
class NotRule(Rule):
def __init__(self, inner):
self.inner = inner
def evaluate(self, context):
return not self.inner.evaluate(context)
def run_rule(rule, context):
"""Evaluate a rule and return (result, checks_run)."""
context["_checks"] = 0
result = rule.evaluate(context)
return result, context["_checks"]
# Define atomic predicates
is_admin = PredicateRule("is_admin", lambda ctx: ctx.get("role") == "admin")
is_owner = PredicateRule("is_owner", lambda ctx: ctx.get("is_owner", False))
is_banned = PredicateRule("is_banned", lambda ctx: ctx.get("banned", False))
has_permission = PredicateRule("has_permission", lambda ctx: ctx.get("has_perm", False))
# Rule 1: is_admin AND has_permission
rule1 = is_admin.AND(has_permission)
print("--- Rule: is_admin AND has_permission ---")
r, c = run_rule(rule1, {"role": "admin", "has_perm": True})
print(f"Admin with permission: {r} ({c} checks run)")
r, c = run_rule(rule1, {"role": "admin", "has_perm": False})
print(f"Admin without permission: {r} ({c} checks run)")
r, c = run_rule(rule1, {"role": "user", "has_perm": True})
print(f"Non-admin with permission: {r} ({c} checks run)")
# Rule 2: is_owner OR is_admin
rule2 = is_owner.OR(is_admin)
print("--- Rule: is_owner OR is_admin ---")
r, c = run_rule(rule2, {"is_owner": True, "role": "user"})
print(f"Owner (not admin): {r} ({c} checks run)")
r, c = run_rule(rule2, {"is_owner": False, "role": "admin"})
print(f"Admin (not owner): {r} ({c} checks run)")
r, c = run_rule(rule2, {"is_owner": False, "role": "user"})
print(f"Neither: {r} ({c} checks run)")
# Rule 3: NOT is_banned
rule3 = is_banned.NOT()
print("--- Rule: NOT is_banned ---")
r, c = run_rule(rule3, {"banned": True})
print(f"Banned user: {r} ({c} checks run)")
r, c = run_rule(rule3, {"banned": False})
print(f"Normal user: {r} ({c} checks run)")
# Rule 4: Complex — (is_admin OR is_owner) AND NOT is_banned AND has_permission
rule4 = (is_admin.OR(is_owner)).AND(is_banned.NOT()).AND(has_permission)
print("--- Complex: (is_admin OR is_owner) AND NOT is_banned AND has_permission ---")
r, c = run_rule(rule4, {"role": "admin", "is_owner": False, "banned": False, "has_perm": True})
print(f"Admin, not banned, has perm: {r} ({c} checks run)")
r, c = run_rule(rule4, {"role": "user", "is_owner": True, "banned": False, "has_perm": True})
print(f"Owner, not banned, has perm: {r} ({c} checks run)")
r, c = run_rule(rule4, {"role": "admin", "is_owner": False, "banned": True, "has_perm": True})
print(f"Admin, banned: {r} ({c} checks run)")
r, c = run_rule(rule4, {"role": "user", "is_owner": False, "banned": False, "has_perm": True})
print(f"Nobody, not banned, has perm: {r} ({c} checks run)")Solution
--- Rule: is_admin AND has_permission ---
Admin with permission: True (2 checks run)
Admin without permission: False (2 checks run)
Non-admin with permission: False (1 checks run)
--- Rule: is_owner OR is_admin ---
Owner (not admin): True (1 checks run)
Admin (not owner): True (2 checks run)
Neither: False (2 checks run)
--- Rule: NOT is_banned ---
Banned user: False (1 checks run)
Normal user: True (1 checks run)
--- Complex: (is_admin OR is_owner) AND NOT is_banned AND has_permission ---
Admin, not banned, has perm: True (4 checks run)
Owner, not banned, has perm: True (4 checks run)
Admin, banned: False (3 checks run)
Nobody, not banned, has perm: False (2 checks run)
How the framework mirrors Python short-circuit semantics:
Each composite rule (AND, OR, NOT) implements the same short-circuit logic as Python's and, or, not:
- AndRule: Evaluates left first. If left is
False, returnsFalseimmediately — right is never evaluated. This isleft and right. - OrRule: Evaluates left first. If left is
True, returnsTrueimmediately — right is never evaluated. This isleft or right. - NotRule: Always evaluates its inner rule (negation cannot short-circuit).
Trace through the complex rule for "Admin, banned":
(is_admin OR is_owner) AND (NOT is_banned) AND has_permission
Step 1: is_admin? YES (check #1) -> OR short-circuits, skip is_owner
Step 2: is_banned? YES (check #2) -> NOT makes it False
Step 3: AND sees False -> short-circuits, skip has_permission
Result: False after 3 checks (saved 1 check)
Trace for "Nobody, not banned, has perm":
Step 1: is_admin? NO (check #1) -> OR continues
Step 2: is_owner? NO (check #2) -> OR returns False
Step 3: AND sees False -> short-circuits everything else
Result: False after 2 checks (saved 2 checks)
Real-world applications:
This pattern appears in authorization frameworks (RBAC/ABAC), feature flag systems, query optimization (SQL WHERE clause evaluation), and rule engines (Drools, business rules). The short-circuit behavior is not just an optimization — it prevents unnecessary side effects like database queries, API calls, or expensive computations.
Expected Output
--- Rule: is_admin AND has_permission ---
Admin with permission: True (2 checks run)
Admin without permission: False (2 checks run)
Non-admin with permission: False (1 checks run)
--- Rule: is_owner OR is_admin ---
Owner (not admin): True (1 checks run)
Admin (not owner): True (2 checks run)
Neither: False (2 checks run)
--- Rule: NOT is_banned ---
Banned user: False (1 checks run)
Normal user: True (1 checks run)
--- Complex: (is_admin OR is_owner) AND NOT is_banned AND has_permission ---
Admin, not banned, has perm: True (4 checks run)
Owner, not banned, has perm: True (4 checks run)
Admin, banned: False (3 checks run)
Nobody, not banned, has perm: False (2 checks run)Hints
Hint 1: Create composable rule objects with AND, OR, NOT operations that build a tree. When evaluated, the tree short-circuits just like Python operators.
Hint 2: Each rule node should track how many checks were run. AND nodes stop at first False, OR nodes stop at first True — mirroring Python short-circuit behavior.
