Codetail

Article 4 of 15

Booleans & Conditions

Truth, logic, decision-making.

15 min read

Comparison operators

Comparisons evaluate to True or False. Python has eight comparison operators, and a few behave in ways worth calling out.

Python
1x = 5
2
3print(x == 5) # True: equal in value
4print(x != 3) # True: not equal
5print(x > 3) # True: greater than
6print(x < 3) # False: less than
7print(x >= 5) # True: greater than or equal
8print(x <= 4) # False: less than or equal

Chained comparisons

Python lets you chain comparisons the way you write them in math. This is unique among mainstream languages and makes range checks read naturally.

Python
1x = 5
2
3# Other languages need: x > 0 and x < 10
4# Python lets you write it directly:
5print(0 < x < 10) # True
6print(1 <= x <= 5) # True
7print(0 < x < 3) # False
8
9# Chains can be longer than two
10print(1 < 2 < 3 < 4) # True: all links must hold
11print(1 < 2 < 2 < 4) # False: 2 < 2 fails

Python evaluates each link once and short-circuits on the first false comparison. In 0 < x < 10, if 0 < xis False, Python doesn't evaluate x < 10 at all.

is and is not: identity, not equality

is checks whether two names point to the same object in memory, not whether they have the same value. Use it only for None, True, and False.

Python
1# Correct use of is: checking for None
2result = None
3if result is None:
4 print("no result yet")
5
6if result is not None:
7 print("got a result")
8
9# Do NOT use is to compare values, it's unreliable
10a = 1000
11b = 1000
12print(a == b) # True: always correct
13print(a is b) # False: different objects (may vary by interpreter)

in and not in: membership

The in operator tests membership. It works on strings, lists, tuples, sets, dicts, and any iterable.

Python
1# Strings: checks substring
2print("py" in "python") # True
3print("Java" in "python") # False
4
5# Lists: checks element
6primes = [2, 3, 5, 7, 11]
7print(5 in primes) # True
8print(4 not in primes) # True
9
10# Dicts: checks keys by default
11config = {"debug": True, "port": 8080}
12print("debug" in config) # True
13print("host" not in config) # True
14
15# Sets: O(1) lookup, fastest for membership tests
16valid_extensions = {".jpg", ".png", ".gif", ".webp"}
17print(".jpg" in valid_extensions) # True

Logical operators

Python has three logical operators: and, or, and not. Most tutorials show them as boolean combiners, true, but the real behavior is more useful than that.

and and or return values, not booleans

and returns the first falsy value it finds, or the last value if all are truthy. or returns the first truthy value it finds, or the last value if all are falsy. This is what makes short-circuit patterns work.

Python
1# and: returns first falsy, or last value
2print(1 and 2) # 2: both truthy, returns last
3print(0 and 2) # 0: 0 is falsy, returned immediately
4print("" and "hi") # "": "" is falsy, returned immediately
5
6# or: returns first truthy, or last value
7print(0 or 2) # 2: 0 is falsy, tries next, returns 2
8print(1 or 2) # 1: 1 is truthy, returned immediately
9print(0 or "") # "": both falsy, returns last
10
11# not: always returns True or False
12print(not True) # False
13print(not 0) # True
14print(not "") # True
15print(not "hello") # False

Short-circuit evaluation

Python stops evaluating as soon as the result is certain. and stops on the first falsy value. or stops on the first truthy value. Expressions to the right never run.

Python
1def expensive():
2 print("running expensive check...")
3 return True
4
5# and: if left side is falsy, right side never runs
6result = False and expensive() # expensive() is never called
7print(result) # False
8
9# or: if left side is truthy, right side never runs
10result = True or expensive() # expensive() is never called
11print(result) # True
12
13# Practical: guard against None before accessing attributes
14user = None
15name = user and user.name # safe, user.name never evaluated if user is None
16print(name) # None

The default value pattern

Because or returns the first truthy value, it's used to provide fallback defaults. This is idiomatic Python you'll see everywhere.

Python
1# Return value or a default
2name = ""
3display = name or "Anonymous"
4print(display) # "Anonymous"
5
6name = "Alice"
7display = name or "Anonymous"
8print(display) # "Alice"
9
10# Function parameter defaults
11def connect(host, port=None):
12 port = port or 443
13 print(f"Connecting to {host}:{port}")
14
15connect("example.com") # Connecting to example.com:443
16connect("example.com", 8080) # Connecting to example.com:8080

Gotcha: the or default pattern fails when 0, False, or an empty list are legitimate values. For example, count = user_count or 0 replaces a valid 0 with 0. In those cases, use an explicit if value is None check.

Conditionals

if statements execute a block when a condition is truthy. Any expression can be a condition, Python calls bool() on it automatically.

Python
1score = 85
2
3if score >= 90:
4 grade = "A"
5elif score >= 80:
6 grade = "B"
7elif score >= 70:
8 grade = "C"
9else:
10 grade = "F"
11
12print(grade) # B

Python uses indentation to define blocks, not curly braces. The standard indent is 4 spaces. Be consistent, mixing tabs and spaces causes a TabError.

Truthiness in conditions

You don't need to write explicit comparisons for empty/zero checks. Python evaluates the value directly.

Python
1items = []
2name = ""
3count = 0
4
5# Verbose (unnecessary)
6if len(items) == 0:
7 print("no items")
8if name == "":
9 print("no name")
10
11# Idiomatic
12if not items:
13 print("no items")
14if not name:
15 print("no name")
16if not count:
17 print("zero count")

Conditional expressions (ternary)

A conditional expression picks one of two values in a single line. Use it for simple assignments. For anything with side effects or multiple branches, use a full if statement.

Python
1age = 20
2
3# Syntax: value_if_true if condition else value_if_false
4status = "adult" if age >= 18 else "minor"
5print(status) # adult
6
7# Useful inline
8def describe(n):
9 return "positive" if n > 0 else "negative" if n < 0 else "zero"
10
11print(describe(5)) # positive
12print(describe(-3)) # negative
13print(describe(0)) # zero
14
15# In f-strings
16score = 73
17print(f"Result: {'pass' if score >= 60 else 'fail'}")

match statements (Python 3.10+)

Python 3.10 added structural pattern matching. It's more powerful than a chain of elifs because it matches structure, not just values.

Python
1command = "quit"
2
3match command:
4 case "quit" | "exit" | "q":
5 print("Goodbye!")
6 case "help":
7 print("Available commands: quit, help, run")
8 case "run":
9 print("Running...")
10 case _:
11 print(f"Unknown command: {command}")
12
13# Matching on structure
14point = (0, 5)
15
16match point:
17 case (0, 0):
18 print("origin")
19 case (x, 0):
20 print(f"on x-axis at {x}")
21 case (0, y):
22 print(f"on y-axis at {y}") # this matches
23 case (x, y):
24 print(f"point at {x}, {y}")

Real-world patterns

Conditional logic you'll write constantly, and the cleaner way to write it.

Guard clauses: exit early

Handle error cases and edge cases at the top of a function, then write the happy path without nesting. Deeply nested conditions are hard to read. Flat is better.

Python
1# Nested, hard to follow
2def process_order(order):
3 if order is not None:
4 if order.items:
5 if order.total > 0:
6 # actual logic buried three levels deep
7 return order.submit()
8
9# Flat with guard clauses, easy to follow
10def process_order(order):
11 if order is None:
12 return None
13 if not order.items:
14 return None
15 if order.total <= 0:
16 return None
17 return order.submit()

all() and any()

Check conditions across a collection without writing a loop. all() returns True if every item is truthy. any() returns True if at least one is. Both short-circuit.

Python
1scores = [88, 92, 74, 95, 81]
2
3# Did everyone pass?
4print(all(s >= 60 for s in scores)) # True
5
6# Did anyone get a perfect score?
7print(any(s == 100 for s in scores)) # False
8
9# Validate a form, all fields filled?
10fields = {"name": "Alice", "email": "alice@example.com", "age": ""}
11print(all(fields.values())) # False: "age" is empty
12
13# Check file extensions
14files = ["report.pdf", "data.csv", "image.png"]
15all_docs = all(f.endswith((".pdf", ".doc", ".txt")) for f in files)
16print(all_docs) # False

Conditional imports

Python
1import sys
2
3# Use different implementations depending on Python version
4if sys.version_info >= (3, 11):
5 import tomllib
6else:
7 try:
8 import tomllib
9 except ImportError:
10 import tomli as tomllib # fallback library
11
12# Platform-specific code
13if sys.platform == "win32":
14 import winreg
15else:
16 winreg = None

Conditions that read like English

Python's operators are words, which lets you write conditions that read almost like prose. Prefer readable over clever.

Python
1age = 25
2is_member = True
3has_coupon = False
4
5# Hard to read
6if (age >= 18 and age <= 65) and (is_member or has_coupon):
7 apply_discount()
8
9# Cleaner with named variables
10is_eligible_age = 18 <= age <= 65
11has_discount_access = is_member or has_coupon
12
13if is_eligible_age and has_discount_access:
14 apply_discount()
15
16# Python reads naturally
17items = ["apple", "banana", "cherry"]
18if "banana" in items and len(items) > 2:
19 print("found banana in a full cart")

Avoid comparing to True or False

Python
1is_active = True
2
3# Redundant, don't do this
4if is_active == True:
5 print("active")
6
7# Idiomatic
8if is_active:
9 print("active")
10
11# For explicit None checks, is/is not is the right tool
12value = None
13
14if value is None: # correct
15 print("no value")
16if not value: # wrong, also catches 0, "", [], etc.
17 print("falsy")