Skip to main content

Python Truthiness and Falsiness Practice Problems & Exercises

Practice: Truthiness and Falsiness

12 problems4 Easy5 Medium3 Hard40–55 min
← Back to lesson

Easy

#1Classify Truthy vs FalsyEasy
truthinessfalsy-valuesbool

Classify each value as truthy or falsy. Some are tricky — pay close attention to containers with falsy contents.

Python
values = [0, "", [], None, {}, 0.0, False, 1, "hello", [0], 0j, (0,)]
labels = ['0', '""', '[]', 'None', '{}', '0.0', 'False', '1', '"hello"', '[0]', '0j', '(0,)']

for label, val in zip(labels, values):
    result = "truthy" if val else "falsy"
    print(f"{label:8s} -> {result}")
Solution
0 -> falsy
"" -> falsy
[] -> falsy
None -> falsy
{} -> falsy
0.0 -> falsy
False -> falsy
1 -> truthy
"hello" -> truthy
[0] -> truthy
0j -> falsy
(0,) -> truthy

Key insights:

  • All numeric zeros are falsy: 0, 0.0, 0j (complex zero), Decimal(0), Fraction(0) — they all evaluate to False.
  • Empty containers are falsy: "", [], {}, (), set(), frozenset() — an empty collection is falsy.
  • Non-empty containers are truthy regardless of contents: [0] is a list with one element (the element happens to be falsy, but the list itself is non-empty and therefore truthy). Same for (0,) — a tuple with one element.
  • None and False are always falsy: These are the two most obvious falsy values.
  • This is Python's boolean protocol — bool(x) is called internally whenever a value appears in a boolean context (if, while, and, or, not).
Expected Output
0        -> falsy
""       -> falsy
[]       -> falsy
None     -> falsy
{}       -> falsy
0.0      -> falsy
False    -> falsy
1        -> truthy
"hello"  -> truthy
[0]      -> truthy
0j       -> falsy
(0,)     -> truthy
Hints

Hint 1: Python has exactly 8 built-in falsy values: None, False, 0, 0.0, 0j, "", (), [], {}, set(), frozenset(), and any numeric zero.

Hint 2: A single-element tuple (0,) is truthy even though the element inside is falsy — truthiness depends on the container, not its contents.

#2Predict the If BehaviorEasy
truthinessif-statementedge-cases

Predict which branch executes for each test case before running the code. These edge cases trip up even experienced developers.

Python
def test_truthiness(label, value, explanation):
    if value:
        print(f"{label}: IF branch ({explanation})")
    else:
        print(f"{label}: ELSE branch ({explanation})")

test_truthiness("Test 1", "", "empty string is falsy")
test_truthiness("Test 2", "   ", "whitespace string is truthy")
test_truthiness("Test 3", 0, "0 is falsy")
test_truthiness("Test 4", -1, "negative number is truthy")
test_truthiness("Test 5", {}, "empty dict is falsy")
test_truthiness("Test 6", {"a": 0}, "dict with falsy value is truthy")
Solution
Test 1: ELSE branch (empty string is falsy)
Test 2: IF branch (whitespace string is truthy)
Test 3: ELSE branch (0 is falsy)
Test 4: IF branch (negative number is truthy)
Test 5: ELSE branch (empty dict is falsy)
Test 6: IF branch (dict with falsy value is truthy)

Common traps:

  • " " (whitespace) is truthy: This catches many people. A string with spaces has len() == 3, so it is truthy. If you need to treat whitespace-only strings as "empty", use if value.strip(): instead.
  • -1 is truthy: Only zero is falsy among numbers. Negative numbers, fractions, huge numbers — all truthy.
  • {"a": 0} is truthy: The dict has one entry. The value inside is falsy (0), but the dict itself is non-empty. Python checks the container's length, not its contents.

Rule of thumb: Truthiness is about the existence of data, not the quality of data inside.

Expected Output
Test 1: ELSE branch (empty string is falsy)
Test 2: IF branch (whitespace string is truthy)
Test 3: ELSE branch (0 is falsy)
Test 4: IF branch (negative number is truthy)
Test 5: ELSE branch (empty dict is falsy)
Test 6: IF branch (dict with falsy value is truthy)
Hints

Hint 1: A string containing only spaces " " is NOT empty — it has length 3, so it is truthy. Only the literal empty string "" is falsy.

Hint 2: Negative numbers like -1 are truthy. Only numeric zero (0, 0.0, 0j) is falsy. A dict like {"a": 0} has one key-value pair, so it is non-empty and truthy.

#3Short-Circuit or — First Truthy ValueEasy
or-operatorshort-circuittruthy

Predict what each or chain returns. Remember: or returns the first truthy value, or the last value if all are falsy.

Python
# Test 1: Skip falsy values, return first truthy
result1 = 0 or "" or [] or "found it"
print(f'Test 1: 0 or "" or [] or "found it" -> {result1}')

# Test 2: Stop at first truthy — never evaluates 99
result2 = "" or 0 or 42 or 99
print(f'Test 2: "" or 0 or 42 or 99        -> {result2}')

# Test 3: ALL falsy — returns last value
result3 = None or "" or 0 or [] or {}
print(f'Test 3: None or "" or 0 or [] or {{}} -> {result3}')

# Test 4: First value is truthy — returns immediately
result4 = "first" or "second"
print(f'Test 4: "first" or "second"         -> {result4}')

# Test 5: All falsy — returns last
result5 = 0 or "" or None
print(f'Test 5: 0 or "" or None             -> {result5}')
Solution
Test 1: 0 or "" or [] or "found it" -> found it
Test 2: "" or 0 or 42 or 99 -> 42
Test 3: None or "" or 0 or [] or {} -> {}
Test 4: "first" or "second" -> first
Test 5: 0 or "" or None -> None

How or short-circuits:

Python evaluates or left to right. The moment it finds a truthy value, it returns that value without evaluating the rest. If every value is falsy, it returns the last value in the chain.

  • Test 1: 0 is falsy, "" is falsy, [] is falsy, "found it" is truthy — returns "found it".
  • Test 2: "" is falsy, 0 is falsy, 42 is truthy — returns 42. Python never evaluates 99.
  • Test 3: Every value is falsy. When all are falsy, or returns the last one: {}.
  • Test 4: "first" is truthy — returns immediately. "second" is never evaluated.
  • Test 5: All falsy — returns the last value: None.

This is the foundation of the value or default idiom — we will use it extensively in the Medium problems.

Expected Output
Test 1: 0 or "" or [] or "found it" -> found it
Test 2: "" or 0 or 42 or 99        -> 42
Test 3: None or "" or 0 or [] or {} -> {}
Test 4: "first" or "second"         -> first
Test 5: 0 or "" or None             -> None
Hints

Hint 1: The `or` operator evaluates left to right and returns the FIRST truthy value. If no value is truthy, it returns the LAST value.

Hint 2: In Test 3, every value is falsy, so `or` returns the very last value in the chain — which is {} (the empty dict).

#4Short-Circuit and — First Falsy ValueEasy
and-operatorshort-circuitfalsy

Predict what each and chain returns. Remember: and returns the first falsy value, or the last value if all are truthy.

Python
# Test 1: All truthy — returns the last value
result1 = "a" and "b" and "c"
print(f'Test 1: "a" and "b" and "c"   -> {result1}')

# Test 2: Stops at first falsy
result2 = "a" and 0 and "c"
print(f'Test 2: "a" and 0 and "c"     -> {result2}')

# Test 3: First value is falsy — returns immediately
result3 = "" and "never reached"
print(f'Test 3: "" and "never reached" -> {result3}')

# Test 4: All truthy integers — returns last
result4 = 1 and 2 and 3 and 4
print(f'Test 4: 1 and 2 and 3 and 4   -> {result4}')

# Test 5: First value is falsy
result5 = [] and {} and None
print(f'Test 5: [] and {{}} and None     -> {result5}')

# Test 6: Short-circuit prevents ZeroDivisionError
result6 = "x" and "" and 1/0  # 1/0 never executes!
print(f'Test 6: "x" and "" and 1/0    -> {result6}')
Solution
Test 1: "a" and "b" and "c" -> c
Test 2: "a" and 0 and "c" -> 0
Test 3: "" and "never reached" ->
Test 4: 1 and 2 and 3 and 4 -> 4
Test 5: [] and {} and None -> []
Test 6: "x" and "" and 1/0 ->

How and short-circuits:

Python evaluates and left to right. The moment it finds a falsy value, it returns that value without evaluating the rest. If every value is truthy, it returns the last value.

  • Test 1: All truthy — returns "c" (the last value).
  • Test 2: "a" is truthy, 0 is falsy — returns 0. Never evaluates "c".
  • Test 3: "" is falsy — returns "" immediately. The string "never reached" is truly never evaluated.
  • Test 4: All truthy integers — returns 4 (the last value).
  • Test 5: [] is falsy — returns [] immediately. Never checks {} or None.
  • Test 6: "x" is truthy, "" is falsy — returns "". The expression 1/0 is never evaluated, so no ZeroDivisionError occurs. This is short-circuit evaluation in action.

Key insight: and and or return actual values, not True/False. This makes them useful for much more than just boolean conditions.

Expected Output
Test 1: "a" and "b" and "c"   -> c
Test 2: "a" and 0 and "c"     -> 0
Test 3: "" and "never reached" -> 
Test 4: 1 and 2 and 3 and 4   -> 4
Test 5: [] and {} and None     -> []
Test 6: "x" and "" and 1/0    -> 
Hints

Hint 1: The `and` operator evaluates left to right and returns the FIRST falsy value. If all values are truthy, it returns the LAST value.

Hint 2: In Test 6, `and` returns the empty string before ever reaching `1/0`. This is short-circuit evaluation — the division by zero never executes.


Medium

#5Implement __bool__ on a Custom ClassMedium
__bool__boolean-protocoldunder-methods

Implement the __bool__ method so that a Wallet is truthy when it contains money (positive balance) and falsy when empty or negative.

Python
class Wallet:
    """A wallet is truthy if it has any money in it."""
    def __init__(self, balance=0.0):
        self.balance = balance

    def __bool__(self):
        return self.balance > 0

    def __repr__(self):
        return f"Wallet(${self.balance:.2f})"

# --- Test it ---
rich = Wallet(100.0)
broke = Wallet(0.0)
debt = Wallet(-50.0)

print(f"{rich} is truthy: {bool(rich)}")
print(f"{broke} is truthy: {bool(broke)}")
print(f"{debt} is truthy: {bool(debt)}")

print(f"bool(rich) = {bool(rich)}")
print(f"bool(broke) = {bool(broke)}")

# Use in if-statement
if rich:
    print("if rich: You can buy something!")
else:
    print("if rich: Your wallet is empty.")

if broke:
    print("if broke: You can buy something!")
else:
    print("if broke: Your wallet is empty.")
Solution
class Wallet:
def __init__(self, balance=0.0):
self.balance = balance

def __bool__(self):
return self.balance > 0

def __repr__(self):
return f"Wallet(${self.balance:.2f})"

How __bool__ works in the boolean protocol:

  1. When Python needs a truth value for an object (in if, while, bool(), and, or, not), it calls __bool__() first.
  2. __bool__ must return True or False — returning anything else raises TypeError.
  3. If __bool__ is not defined, Python falls back to __len__() — an object with len() == 0 is falsy.
  4. If neither __bool__ nor __len__ is defined, the object is always truthy.

Design decisions:

  • We chose self.balance > 0 (not self.balance != 0) because a negative balance (debt) should not count as "having money."
  • This is a common pattern: model domain logic in __bool__ so that conditional checks read naturally — if wallet: means "if the wallet has funds."
class Wallet:
    """A wallet is truthy if it has any money in it."""
    def __init__(self, balance=0.0):
        self.balance = balance

    def __bool__(self):
        # YOUR CODE HERE: return True if balance > 0
        pass

    def __repr__(self):
        return f"Wallet($\{self.balance:.2f})"
Expected Output
Wallet($100.00) is truthy: True
Wallet($0.00) is truthy: False
Wallet($-50.00) is truthy: False
bool(rich) = True
bool(broke) = False
if rich: You can buy something!
if broke: Your wallet is empty.
Hints

Hint 1: The __bool__ method must return True or False. Python calls it whenever your object appears in a boolean context (if, while, bool(), and, or, not).

Hint 2: Return self.balance > 0 — this is a comparison that naturally produces True or False.

#6__len__ Fallback When __bool__ Is AbsentMedium
__len____bool__fallbackboolean-protocol

Demonstrate that Python falls back to __len__ for truth testing when __bool__ is not defined. Then show that __bool__ takes priority when both exist.

Python
class TaskQueue:
    """No __bool__ defined — Python will use __len__ for truth testing."""
    def __init__(self, tasks=None):
        self._tasks = tasks or []

    def __len__(self):
        return len(self._tasks)

    def __repr__(self):
        return f"TaskQueue({len(self)} tasks)"

full_queue = TaskQueue(["deploy", "test", "review"])
empty_queue = TaskQueue()

print(f"{full_queue} -> bool = {bool(full_queue)}")
print(f"{empty_queue} -> bool = {bool(empty_queue)}")
print(f"len(full_queue) = {len(full_queue)}")
print(f"len(empty_queue) = {len(empty_queue)}")

# Prove which method Python is using
print("--- Fallback chain ---")
print(f"Has __bool__? {hasattr(TaskQueue, '__bool__')}")
print(f"Has __len__?  {hasattr(TaskQueue, '__len__')}")
print("Python uses __len__ as fallback for truth testing")

# Now show __bool__ takes priority
print("--- Proving the fallback ---")

class CustomBool:
    """Has BOTH __bool__ and __len__. __bool__ wins."""
    def __bool__(self):
        return False  # Always falsy

    def __len__(self):
        return 5  # Non-zero, would be truthy

obj = CustomBool()
print(f"With __bool__ defined: CustomBool is always falsy: {bool(obj)}")
print(f"With __bool__ defined: even though len = {len(obj)}")
Solution

Python's boolean protocol has a strict priority chain:

  1. __bool__ is checked first. If defined, its return value determines truthiness.
  2. __len__ is the fallback. If __bool__ is absent but __len__ exists, Python calls len(obj) and treats 0 as falsy, non-zero as truthy.
  3. Neither defined: The object is always truthy.
Priority: __bool__ > __len__ > always True

Why this matters in practice:

  • Built-in containers (list, dict, set, str) define __len__ — that is why empty containers are falsy.
  • If you create a custom container, implementing __len__ gives you truthiness for free.
  • If you need truthiness to depend on something other than length (like a Wallet balance), define __bool__ explicitly.

Common mistake: Defining __bool__ to return a non-boolean value. Python requires __bool__ to return exactly True or False — returning an integer or string raises TypeError.

Expected Output
TaskQueue(3 tasks) -> bool = True
TaskQueue(0 tasks) -> bool = False
len(full_queue) = 3
len(empty_queue) = 0
--- Fallback chain ---
Has __bool__? False
Has __len__?  True
Python uses __len__ as fallback for truth testing
--- Proving the fallback ---
With __bool__ defined: CustomBool is always falsy: False
With __bool__ defined: even though len = 5
Hints

Hint 1: If a class defines __len__ but NOT __bool__, Python uses __len__() to determine truthiness: zero means falsy, non-zero means truthy.

Hint 2: If both __bool__ and __len__ exist, __bool__ wins. __len__ is only the fallback.

#7Default Values with the or IdiomMedium
or-idiomdefault-valuestruthy

Use the or idiom for default values, then demonstrate the dangerous gotcha where it silently discards valid falsy inputs.

Python
# --- The useful idiom ---
def get_greeting(name):
    name = name or "Stranger"
    return f"Hello, {name}!"

print(f'get_greeting("Alice")   -> {get_greeting("Alice")}')
print(f'get_greeting("")        -> {get_greeting("")}')
print(f'get_greeting(None)      -> {get_greeting(None)}')

def get_config(overrides):
    defaults = {"timeout": 10, "retries": 3}
    return overrides or defaults

print(f'get_config({{"timeout": 30}}) -> {get_config({"timeout": 30})}')
print(f'get_config({{}})              -> {get_config({})}')
print(f'get_config(None)            -> {get_config(None)}')

# --- The gotcha ---
print("--- The gotcha ---")

def get_port_dangerous(port):
    """BUG: or treats 0 as falsy, so port=0 gets replaced."""
    return port or 8080

def get_port_safe(port):
    """FIX: explicit None check preserves valid 0."""
    return port if port is not None else 8080

print(f"Dangerous: get_port(0)  -> {get_port_dangerous(0)} (0 was a valid value!)")
print(f"Safe:      get_port(0)  -> {get_port_safe(0)} (None-check preserves 0)")
Solution

The value or default idiom:

# Simple and clean for strings and collections
name = name or "Unknown"
items = items or []
config = config or {}

When it breaks:

# DANGEROUS — 0 is a legitimate port number
port = port or 8080 # port=0 becomes 8080!

# DANGEROUS — False is a legitimate setting
verbose = verbose or True # verbose=False becomes True!

# DANGEROUS — "" might be a valid empty-string input
value = value or "default" # "" becomes "default"

The safe alternative:

# Use `is not None` check when falsy values are valid
port = port if port is not None else 8080
verbose = verbose if verbose is not None else True

Rule of thumb: Use or for defaults when you know the input will never legitimately be 0, False, or "". Use is not None when those falsy values are valid inputs.

Expected Output
get_greeting("Alice")   -> Hello, Alice!
get_greeting("")        -> Hello, Stranger!
get_greeting(None)      -> Hello, Stranger!
get_config({"timeout": 30}) -> {'timeout': 30}
get_config({})              -> {'timeout': 10, 'retries': 3}
get_config(None)            -> {'timeout': 10, 'retries': 3}
--- The gotcha ---
Dangerous: get_port(0)  -> 8080 (0 was a valid value!)
Safe:      get_port(0)  -> 0 (None-check preserves 0)
Hints

Hint 1: The pattern `value or default` returns `default` whenever `value` is falsy. This works great for strings and collections but has a gotcha with 0 and False.

Hint 2: When 0, False, or "" could be valid inputs, use `value if value is not None else default` instead of `value or default`.

#8Truthiness Gotchas — Empty String vs None vs ZeroMedium
gotchasNoneempty-stringzeroidentity

Explore the subtle differences between falsy values. They all evaluate to False in boolean context, but they are NOT all equal to each other.

Python
# --- Equality gotchas ---
print("--- Equality gotchas ---")
print(f"0 == False:  {0 == False!s:5s} (they are equal!)")
print(f'"" == False: {("" == False)!s:5s} (not equal)')
print(f"[] == False: {([] == False)!s:5s} (not equal)")
print(f"None == 0:   {(None == 0)!s:5s} (not equal)")
print(f'None == "":  {(None == "")!s:5s} (not equal)')
print(f"0 == 0.0:    {(0 == 0.0)!s:5s} (int and float zero are equal)")

# --- Identity vs truthiness ---
print("--- Identity vs truthiness ---")
print(f"0 is False:  {(0 is False)!s:5s} (different objects)")
print(f"0 is 0:      {(0 is 0)!s:5s}  (same int cache)")
print(f"None is None: {(None is None)!s:4s} (singleton)")

# --- bool() unifies them ---
print("--- bool() vs == ---")
all_same_bool = bool(0) == bool(False) == bool("") == bool([])
print(f"bool(0) == bool(False) == bool(\"\") == bool([]): {all_same_bool}")
print("All falsy, but not all equal to each other")
Solution

Why 0 == False is True:

In Python, bool is a subclass of int. Literally: isinstance(False, int) returns True. False is 0 and True is 1 in the integer hierarchy. This means:

False + False # 0
True + True # 2
True * 10 # 10

The equality matrix of falsy values:

None False 0 0.0 "" [] {}
None == != != != != != !=
False != == == == != != !=
0 != == == == != != !=
0.0 != == == == != != !=
"" != != != != == != !=
[] != != != != != == !=
{} != != != != != != ==

Practical rules:

  • Use is None (not == None) to check for None.
  • Use == 0 or is False depending on intent — but be aware that 0 == False is True.
  • Never compare collections to False — use if not my_list: instead.
  • bool() is the great equalizer — it maps all falsy values to False and all truthy values to True.
Expected Output
--- Equality gotchas ---
0 == False:  True   (they are equal!)
"" == False: False  (not equal)
[] == False: False  (not equal)
None == 0:   False  (not equal)
None == "":  False  (not equal)
0 == 0.0:    True   (int and float zero are equal)
--- Identity vs truthiness ---
0 is False:  False  (different objects)
0 is 0:      True   (same int cache)
None is None: True  (singleton)
--- bool() vs == ---
bool(0) == bool(False) == bool("") == bool([]): True
All falsy, but not all equal to each other
Hints

Hint 1: 0 == False is True because bool is a subclass of int in Python: False is literally 0 and True is literally 1.

Hint 2: None is not equal to any other falsy value. None == 0 is False, None == "" is False. Use `is None` for None checks, not `==`.

#9Build a Filter Using TruthinessMedium
filtertruthinesslist-comprehensiondata-cleaning

Use truthiness-based filtering to clean messy data. Implement three different filters that demonstrate progressively finer control.

Python
data = [
    "Alice", "", "Bob", None, "Charlie", 0, "Diana", [], "", "Eve", False, "Frank"
]

print(f"Original: {data}")

# Method 1: filter(None, ...) uses truthiness
cleaned_filter = list(filter(None, data))
print(f"Filter falsy (bool):     {cleaned_filter}")

# Method 2: List comprehension with implicit truthiness
cleaned_compact = [x for x in data if x]
print(f"Filter falsy (compact):  {cleaned_compact}")

# Method 3: Only truthy strings
truthy_strings = [x for x in data if isinstance(x, str) and x]
print(f"Truthy strings only:     {truthy_strings}")

# Method 4: ALL strings, including empty ones
all_strings = [x for x in data if isinstance(x, str)]
print(f"All strings (inc empty): {all_strings}")
Solution

Three levels of filtering:

# Level 1: Remove all falsy values (None, 0, "", [], False)
cleaned = [x for x in data if x]
# or: list(filter(None, data))

# Level 2: Only truthy values of a specific type
names = [x for x in data if isinstance(x, str) and x]

# Level 3: All values of a specific type (regardless of truthiness)
all_strings = [x for x in data if isinstance(x, str)]

When to use each:

  • filter(None, data) — Quick data cleaning when you want to remove all "empty" or "missing" values. Common in CSV processing, API responses, form data.
  • [x for x in data if x] — Same thing but more Pythonic and easier to extend with additional conditions.
  • isinstance check — When the type matters more than truthiness. Preserves "" and 0 as valid values of their respective types.

Production pattern — cleaning API responses:

def clean_tags(raw_tags):
"""Remove None and empty strings from a tag list."""
return [tag.strip() for tag in raw_tags if isinstance(tag, str) and tag.strip()]
data = [
    "Alice", "", "Bob", None, "Charlie", 0, "Diana", [], "", "Eve", False, "Frank"
]

# Filter out all falsy values using truthiness
cleaned = ???

# Filter keeping only truthy strings
names = ???

# Filter keeping all strings (including empty ones)
all_strings = ???
Expected Output
Original: ['Alice', '', 'Bob', None, 'Charlie', 0, 'Diana', [], '', 'Eve', False, 'Frank']
Filter falsy (bool):     ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank']
Filter falsy (compact):  ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank']
Truthy strings only:     ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank']
All strings (inc empty): ['Alice', '', 'Bob', 'Charlie', 'Diana', '', 'Eve', 'Frank']
Hints

Hint 1: filter(None, iterable) uses truthiness to filter — it removes all falsy values. Equivalent to [x for x in iterable if x].

Hint 2: To keep ALL strings including empty ones, use isinstance(x, str) — that checks the type regardless of truthiness.


Hard

#10Connection Pool with State-Dependent BoolHard
__bool__state-machineresource-managementdesign-pattern

Build a ConnectionPool where bool() reflects whether the pool can serve a connection right now. The pool should be truthy only when it is open AND has available connections.

Python
class ConnectionPool:
    def __init__(self, size):
        self._available = [f"Connection-{i+1}" for i in range(size)]
        self._in_use = set()
        self._closed = False

    def __bool__(self):
        """Truthy only if the pool is open and has free connections."""
        return not self._closed and len(self._available) > 0

    def __repr__(self):
        return (
            f"ConnectionPool("
            f"available={len(self._available)}, "
            f"in_use={len(self._in_use)}, "
            f"closed={self._closed})"
        )

    def acquire(self):
        if self._closed:
            raise RuntimeError("Pool is closed")
        if not self._available:
            raise RuntimeError("No connections available")
        conn = self._available.pop(0)
        self._in_use.add(conn)
        return conn

    def release(self, conn):
        if conn not in self._in_use:
            raise ValueError(f"Unknown connection: {conn}")
        self._in_use.remove(conn)
        if not self._closed:
            self._available.append(conn)

    def close(self):
        self._closed = True
        self._available.clear()
        self._in_use.clear()


# --- Test the pool ---
pool = ConnectionPool(3)
print("--- Initial state ---")
print(pool)
print(f"bool(pool) = {bool(pool)} (has available connections)")

print("--- Use connections ---")
conns = []
for _ in range(3):
    c = pool.acquire()
    conns.append(c)
    print(f"Acquired: {c}")
print(pool)
print(f"bool(pool) = {bool(pool)} (no available connections)")

print("--- Release one ---")
pool.release(conns[1])
print(f"Released: {conns[1]}")
print(pool)
print(f"bool(pool) = {bool(pool)} (has available connections)")

print("--- Close pool ---")
pool.close()
print(pool)
print(f"bool(pool) = {bool(pool)} (pool is closed)")

# --- The guard pattern ---
print("--- Guard pattern ---")
pool2 = ConnectionPool(1)
if pool2:
    c = pool2.acquire()
    print(f"Connection acquired: {c}")
if pool2:
    c = pool2.acquire()
    print(f"Connection acquired: {c}")
else:
    print("No connections available — pool exhausted or closed")
Solution

Design rationale:

The __bool__ method encodes business logic — "can this pool serve a request right now?" — into a single truth value. This enables the guard pattern:

if pool:
conn = pool.acquire()

State transitions that affect truthiness:

[Open, 3 available] --acquire--> [Open, 2 available] (still truthy)
[Open, 1 available] --acquire--> [Open, 0 available] (now falsy)
[Open, 0 available] --release--> [Open, 1 available] (truthy again)
[Open, any] --close--> [Closed, 0] (permanently falsy)

Why this pattern is powerful:

  1. Readable conditionals: if pool: reads as natural English — "if the pool has connections."
  2. Works with and/or: conn = pool and pool.acquire() — safe chaining.
  3. Works with while: while pool: process(pool.acquire()) — drain the pool.

Production considerations:

  • In real code, you would use threading.Lock to make acquire/release thread-safe.
  • The __bool__ method should be fast — it is called frequently in conditional checks.
  • Consider implementing as a context manager (__enter__/__exit__) for automatic release.
Expected Output
--- Initial state ---
ConnectionPool(available=3, in_use=0, closed=False)
bool(pool) = True (has available connections)
--- Use connections ---
Acquired: Connection-1
Acquired: Connection-2
Acquired: Connection-3
ConnectionPool(available=0, in_use=3, closed=False)
bool(pool) = False (no available connections)
--- Release one ---
Released: Connection-2
ConnectionPool(available=1, in_use=2, closed=False)
bool(pool) = True (has available connections)
--- Close pool ---
ConnectionPool(available=0, in_use=0, closed=True)
bool(pool) = False (pool is closed)
--- Guard pattern ---
Connection acquired: Connection-2
No connections available — pool exhausted or closed
Hints

Hint 1: The __bool__ method should return True only when the pool is open AND has available connections. This lets you write `if pool: conn = pool.acquire()`.

Hint 2: Track three pieces of state: a list of available connections, a set of in-use connections, and a closed flag. __bool__ checks `not self._closed and len(self._available) > 0`.

#11Truth Table Generator for Compound ExpressionsHard
truth-tableboolean-logicevalcompound-expressions

Build a truth table generator that takes a boolean expression and prints all possible input/output combinations. Support 2 and 3 variables with and, or, and not.

Python
from itertools import product

def truth_table(expression_name, variables, expression_fn):
    """Generate and print a truth table for any boolean expression.

    Args:
        expression_name: Human-readable name of the expression
        variables: List of variable names ['a', 'b'] or ['a', 'b', 'c']
        expression_fn: Function that takes variable values and returns bool
    """
    n = len(variables)
    header = "  ".join(f"{v:6s}" for v in variables) + "  result"
    print(f"=== Truth Table: {expression_name} ===")
    print(f"  {header}")

    for combo in product([True, False], repeat=n):
        values = dict(zip(variables, combo))
        result = expression_fn(values)
        row = "  ".join(f"{str(v):6s}" for v in combo)
        print(f"  {row} {str(result):6s}")

# --- 2-variable tables ---
truth_table(
    "a and b",
    ["a", "b"],
    lambda v: v["a"] and v["b"]
)

print()
truth_table(
    "a or b",
    ["a", "b"],
    lambda v: v["a"] or v["b"]
)

print()
truth_table(
    "a and not b",
    ["a", "b"],
    lambda v: v["a"] and not v["b"]
)

# --- XOR (exclusive or) ---
print()
truth_table(
    "(a or b) and not (a and b)",
    ["a", "b"],
    lambda v: (v["a"] or v["b"]) and not (v["a"] and v["b"])
)
print("This is XOR!")

# --- 3-variable table ---
print()
truth_table(
    "a and (b or c)",
    ["a", "b", "c"],
    lambda v: v["a"] and (v["b"] or v["c"])
)
Solution

Architecture of the truth table generator:

Variables: [a, b] Expression: a and b
│ │
▼ ▼
product([T, F], repeat=2) lambda v: v["a"] and v["b"]
│ │
├── (T, T) ─────────────── True
├── (T, F) ─────────────── False
├── (F, T) ─────────────── False
└── (F, F) ─────────────── False

Key design decisions:

  1. No eval(): We use lambda functions instead of string evaluation. This is safer and faster.
  2. itertools.product: Generates all 2^n combinations efficiently without manual nested loops.
  3. Dict-based variable binding: dict(zip(variables, combo)) creates {"a": True, "b": False} — clean mapping from names to values.

Extending this:

# Adding NAND (universal gate)
truth_table("not (a and b)", ["a", "b"],
lambda v: not (v["a"] and v["b"]))

# Implication (a implies b)
truth_table("not a or b", ["a", "b"],
lambda v: not v["a"] or v["b"])

# 4-variable expression (16 rows)
truth_table("(a and b) or (c and d)", ["a", "b", "c", "d"],
lambda v: (v["a"] and v["b"]) or (v["c"] and v["d"]))

Why truthiness matters here: Python's and/or operators return actual values, not just True/False. But when the inputs ARE boolean, the results are boolean too. The truth table works because we feed in True/False directly.

Expected Output
=== Truth Table: a and b ===
  a      b      result
  True   True   True  
  True   False  False 
  False  True   False 
  False  False  False 

=== Truth Table: a or b ===
  a      b      result
  True   True   True  
  True   False  True  
  False  True   True  
  False  False  False 

=== Truth Table: a and not b ===
  a      b      result
  True   True   False 
  True   False  True  
  False  True   False 
  False  False  False 

=== Truth Table: (a or b) and not (a and b) ===
  a      b      result
  True   True   False 
  True   False  True  
  False  True   True  
  False  False  False 
This is XOR!

=== 3-variable: a and (b or c) ===
  a      b      c      result
  True   True   True   True  
  True   True   False  True  
  True   False  True   True  
  True   False  False  False 
  False  True   True   False 
  False  True   False  False 
  False  False  True   False 
  False  False  False  False
Hints

Hint 1: Use itertools.product([True, False], repeat=n) to generate all combinations of truth values for n variables.

Hint 2: Instead of using eval(), create a mapping dict of variable names to values and pass it to the expression function. This avoids security concerns with eval.

#12Validation Framework with Truthy/Falsy ChainsHard
validationframeworkand-chainor-chaindesign-pattern

Build a validation framework that uses truthiness at every level — rules return truthy/falsy, chains short-circuit, and the validator itself is truthy when all rules pass.

Python
class ValidationResult:
    """Truthy when valid, falsy when there are errors."""
    def __init__(self):
        self.errors = []

    def __bool__(self):
        return len(self.errors) == 0

    def add_error(self, message):
        self.errors.append(message)

    def __repr__(self):
        if self:
            return "ValidationResult(VALID)"
        return f"ValidationResult(INVALID: {self.errors})"


class Validator:
    """Collects validation rules and runs them against data."""
    def __init__(self):
        self._rules = []

    def add_rule(self, check_fn, error_message):
        """Add a rule: check_fn(data) should return truthy for pass."""
        self._rules.append((check_fn, error_message))
        return self  # Enable chaining

    def validate(self, data):
        result = ValidationResult()
        for check_fn, error_msg in self._rules:
            if check_fn(data):
                print(f"  [PASS] {error_msg}")
            else:
                print(f"  [FAIL] {error_msg}")
                result.add_error(error_msg)
        return result

    def first_error(self, data):
        """Short-circuit: return first error or None."""
        for check_fn, error_msg in self._rules:
            if not check_fn(data):
                return error_msg
        return None


# --- Build the validator ---
validator = Validator()
validator.add_rule(
    lambda d: d.get("name"),         # truthy if name exists and non-empty
    "Name is required"
).add_rule(
    lambda d: d.get("email"),        # truthy if email exists and non-empty
    "Email is required"
).add_rule(
    lambda d: d.get("age", 0) > 0,  # truthy if age is positive
    "Age must be positive"
)

# --- Test cases ---
test_cases = [
    {"name": "Alice", "email": "[email protected]", "age": 30},
    {"name": "", "email": "[email protected]", "age": 25},
    {"name": "Charlie", "email": "", "age": -5},
    {},
]

for data in test_cases:
    print(f"=== Validating: {data} ===")
    result = validator.validate(data)
    if result:  # ValidationResult.__bool__
        print(f"  Result: VALID")
    else:
        print(f"  Result: INVALID ({len(result.errors)} error)")
    print()

# --- Short-circuit validation ---
print("=== Short-circuit validation ===")
first_err = validator.first_error({"name": "", "email": "[email protected]", "age": 10})
print(f"First error only: {first_err}")

# --- Chained or-validation ---
print("\n=== Chained or-validation (first valid source wins) ===")

def display_name(user):
    """Return the first truthy value from multiple fallback sources."""
    return user.get("name") or user.get("email") or user.get("username") or "Anonymous"

print(f"Display name: {display_name({'name': 'Alice', 'email': '[email protected]'})}")
print(f"Display name: {display_name({'name': '', 'email': '[email protected]'})}")
print(f"Display name: {display_name({'name': '', 'email': ''})}")
Solution

How truthiness powers every layer of this framework:

Layer 1 — Rules: lambda d: d.get("name") → truthy/falsy
Layer 2 — Results: ValidationResult.__bool__() → truthy if no errors
Layer 3 — Short-circuit: first_error() uses `not check_fn(data)` to bail early
Layer 4 — Defaults: `or` chains for fallback values

Design pattern — Validator with fluent API:

validator = (Validator()
.add_rule(lambda d: d.get("name"), "Name required")
.add_rule(lambda d: d.get("email"), "Email required")
.add_rule(lambda d: d.get("age", 0) > 0, "Age must be positive"))

The add_rule method returns self to enable chaining — a pattern made natural by Python's truthiness system.

Why d.get("name") works as a validator:

  • d.get("name") returns None if the key is missing (falsy).
  • Returns "" if the key exists but is empty (falsy).
  • Returns "Alice" if the key has content (truthy).

One expression covers three failure modes: missing key, None value, and empty string.

Production extensions:

# Type-aware rules
validator.add_rule(
lambda d: isinstance(d.get("age"), int) and d["age"] > 0,
"Age must be a positive integer"
)

# Dependent rules (only run if previous passed)
validator.add_dependent_rule(
depends_on="Email is required",
check=lambda d: "@" in d["email"],
error="Email must contain @"
)

The or-chain fallback pattern (display_name) is used extensively in real applications: user display names, configuration resolution, template rendering, and API response shaping.

Expected Output
=== Validating: {'name': 'Alice', 'email': '[email protected]', 'age': 30} ===
  [PASS] Name is required
  [PASS] Email is required
  [PASS] Age must be positive
  Result: VALID

=== Validating: {'name': '', 'email': '[email protected]', 'age': 25} ===
  [FAIL] Name is required
  Result: INVALID (1 error)

=== Validating: {'name': 'Charlie', 'email': '', 'age': -5} ===
  [FAIL] Email is required
  [FAIL] Age must be positive
  Result: INVALID (2 errors)

=== Validating: {} ===
  [FAIL] Name is required
  [FAIL] Email is required
  [FAIL] Age must be positive
  Result: INVALID (3 errors)

=== Short-circuit validation ===
First error only: Name is required

=== Chained or-validation (first valid source wins) ===
Display name: Alice
Display name: [email protected]
Display name: Anonymous
Hints

Hint 1: Build a Validator class that collects rules as (condition_fn, error_message) pairs. Each rule returns truthy for pass, falsy for fail.

Hint 2: For short-circuit validation, use `and` chaining or `all()` with a generator. For first-valid-source, use `or` chaining across multiple fields.

© 2026 EngineersOfAI. All rights reserved.