Comparison operators
Comparisons evaluate to True or False. Python has eight comparison operators, and a few behave in ways worth calling out.
1x = 523print(x == 5) # True: equal in value4print(x != 3) # True: not equal5print(x > 3) # True: greater than6print(x < 3) # False: less than7print(x >= 5) # True: greater than or equal8print(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.
1x = 523# Other languages need: x > 0 and x < 104# Python lets you write it directly:5print(0 < x < 10) # True6print(1 <= x <= 5) # True7print(0 < x < 3) # False89# Chains can be longer than two10print(1 < 2 < 3 < 4) # True: all links must hold11print(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.
1# Correct use of is: checking for None2result = None3if result is None:4 print("no result yet")56if result is not None:7 print("got a result")89# Do NOT use is to compare values, it's unreliable10a = 100011b = 100012print(a == b) # True: always correct13print(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.
1# Strings: checks substring2print("py" in "python") # True3print("Java" in "python") # False45# Lists: checks element6primes = [2, 3, 5, 7, 11]7print(5 in primes) # True8print(4 not in primes) # True910# Dicts: checks keys by default11config = {"debug": True, "port": 8080}12print("debug" in config) # True13print("host" not in config) # True1415# Sets: O(1) lookup, fastest for membership tests16valid_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.
1# and: returns first falsy, or last value2print(1 and 2) # 2: both truthy, returns last3print(0 and 2) # 0: 0 is falsy, returned immediately4print("" and "hi") # "": "" is falsy, returned immediately56# or: returns first truthy, or last value7print(0 or 2) # 2: 0 is falsy, tries next, returns 28print(1 or 2) # 1: 1 is truthy, returned immediately9print(0 or "") # "": both falsy, returns last1011# not: always returns True or False12print(not True) # False13print(not 0) # True14print(not "") # True15print(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.
1def expensive():2 print("running expensive check...")3 return True45# and: if left side is falsy, right side never runs6result = False and expensive() # expensive() is never called7print(result) # False89# or: if left side is truthy, right side never runs10result = True or expensive() # expensive() is never called11print(result) # True1213# Practical: guard against None before accessing attributes14user = None15name = user and user.name # safe, user.name never evaluated if user is None16print(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.
1# Return value or a default2name = ""3display = name or "Anonymous"4print(display) # "Anonymous"56name = "Alice"7display = name or "Anonymous"8print(display) # "Alice"910# Function parameter defaults11def connect(host, port=None):12 port = port or 44313 print(f"Connecting to {host}:{port}")1415connect("example.com") # Connecting to example.com:44316connect("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.
1score = 8523if score >= 90:4 grade = "A"5elif score >= 80:6 grade = "B"7elif score >= 70:8 grade = "C"9else:10 grade = "F"1112print(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.
1items = []2name = ""3count = 045# Verbose (unnecessary)6if len(items) == 0:7 print("no items")8if name == "":9 print("no name")1011# Idiomatic12if 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.
1age = 2023# Syntax: value_if_true if condition else value_if_false4status = "adult" if age >= 18 else "minor"5print(status) # adult67# Useful inline8def describe(n):9 return "positive" if n > 0 else "negative" if n < 0 else "zero"1011print(describe(5)) # positive12print(describe(-3)) # negative13print(describe(0)) # zero1415# In f-strings16score = 7317print(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.
1command = "quit"23match 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}")1213# Matching on structure14point = (0, 5)1516match 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 matches23 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.
1# Nested, hard to follow2def process_order(order):3 if order is not None:4 if order.items:5 if order.total > 0:6 # actual logic buried three levels deep7 return order.submit()89# Flat with guard clauses, easy to follow10def process_order(order):11 if order is None:12 return None13 if not order.items:14 return None15 if order.total <= 0:16 return None17 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.
1scores = [88, 92, 74, 95, 81]23# Did everyone pass?4print(all(s >= 60 for s in scores)) # True56# Did anyone get a perfect score?7print(any(s == 100 for s in scores)) # False89# Validate a form, all fields filled?10fields = {"name": "Alice", "email": "alice@example.com", "age": ""}11print(all(fields.values())) # False: "age" is empty1213# Check file extensions14files = ["report.pdf", "data.csv", "image.png"]15all_docs = all(f.endswith((".pdf", ".doc", ".txt")) for f in files)16print(all_docs) # False
Conditional imports
1import sys23# Use different implementations depending on Python version4if sys.version_info >= (3, 11):5 import tomllib6else:7 try:8 import tomllib9 except ImportError:10 import tomli as tomllib # fallback library1112# Platform-specific code13if sys.platform == "win32":14 import winreg15else: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.
1age = 252is_member = True3has_coupon = False45# Hard to read6if (age >= 18 and age <= 65) and (is_member or has_coupon):7 apply_discount()89# Cleaner with named variables10is_eligible_age = 18 <= age <= 6511has_discount_access = is_member or has_coupon1213if is_eligible_age and has_discount_access:14 apply_discount()1516# Python reads naturally17items = ["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
1is_active = True23# Redundant, don't do this4if is_active == True:5 print("active")67# Idiomatic8if is_active:9 print("active")1011# For explicit None checks, is/is not is the right tool12value = None1314if value is None: # correct15 print("no value")16if not value: # wrong, also catches 0, "", [], etc.17 print("falsy")