if / elif / else
Python uses indentation instead of braces to define blocks. The colon (:) at the end of a condition starts the block.
# Basic if/else
age = 20
if age >= 18:
print("Adult")
else:
print("Minor")
# if/elif/else chain
score = 85
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
elif score >= 60:
grade = "D"
else:
grade = "F"
print(f"Grade: {grade}") # Grade: B
# Nested if
logged_in = True
is_admin = False
if logged_in:
if is_admin:
print("Admin dashboard")
else:
print("User dashboard")
else:
print("Please log in")
# Chained comparisons
x = 15
if 10 < x < 20: # Python allows this - very readable
print("x is between 10 and 20")
# Multiple conditions
username = "alice"
password = "secret"
if username == "alice" and password == "secret":
print("Welcome, Alice!")
elif username == "alice" or username == "admin":
print("Known user, wrong password")
else:
print("Unknown user")
# One-liner if (only for very simple cases)
x = 5
if x > 0: print("positive") # valid but not recommended for complex code
Truthiness
Python evaluates any object in a boolean context. Understanding what is falsy lets you write idiomatic, concise conditionals.
# Falsy values - evaluate to False in a boolean context
falsy_values = [
False, None,
0, 0.0, 0j, # numeric zeros
"", b"", [], (), {}, set(), # empty containers
]
for v in falsy_values:
print(f"{repr(v):<12} -> {bool(v)}")
# Everything else is truthy
truthy_values = [True, 1, -1, 0.001, "a", " ", [0], {"k": None}]
for v in truthy_values:
print(f"{repr(v):<15} -> {bool(v)}")
# Idiomatic Python conditionals
items = []
if not items: # instead of: if len(items) == 0:
print("List is empty")
name = "Alice"
if name: # instead of: if name != "":
print(f"Hello, {name}!")
config = {"debug": False}
if config: # dict is truthy even if values are falsy
print("Config loaded")
value = None
if value is None: # use 'is None', not truthiness, for None checks
print("No value")
# Default values using 'or'
user_input = ""
display_name = user_input or "Anonymous" # "Anonymous" (input is falsy)
print(display_name)
count = 0
safe_count = count or 1 # 1 (count is 0, falsy)
print(safe_count)
# WARNING: 'or' default fails if 0 is a valid value
# Use explicit None check instead
def get_count():
return 0
result = get_count()
value = result if result is not None else 1 # correct: 0 stays 0
# NOT: value = result or 1 (would replace 0 with 1 - wrong!)
When a variable can legitimately be 0, "", or [], do not test truthiness to detect "no value" - test is None explicitly. Truthiness checks are safe for "does this collection have items?" but unreliable for "was a value provided?"
Ternary Expression
Python's ternary is an expression (returns a value), not a statement. The syntax is: value_if_true if condition else value_if_false.
age = 20
# Ternary expression
label = "adult" if age >= 18 else "minor"
print(label) # adult
# In a print statement
print("yes" if age > 0 else "no")
# Inline assignment
score = 75
result = "pass" if score >= 60 else "fail"
# Nested ternary - avoid for readability
grade = "A" if score >= 90 else "B" if score >= 80 else "C" if score >= 70 else "D" if score >= 60 else "F"
# Hard to read - use if/elif/else instead
# Good use: simple, obvious conditions
items = [1, 2, 3]
msg = f"Found {len(items)} item{'s' if len(items) != 1 else ''}"
print(msg) # Found 3 items
# In a list comprehension
nums = [-3, -1, 0, 2, 5]
abs_vals = [x if x >= 0 else -x for x in nums]
print(abs_vals) # [3, 1, 0, 2, 5]
# As a function argument
name = None
print(f"Hello, {name or 'stranger'}!") # Hello, stranger!
match-case (Python 3.10+)
The match statement is Python's structural pattern matching. It is more powerful than a traditional switch - it can match values, types, structures, and use guards.
# Basic value matching (like switch)
command = "quit"
match command:
case "quit":
print("Quitting...")
case "help":
print("Available commands: quit, help, run")
case "run":
print("Running...")
case _: # wildcard - matches anything (like default)
print(f"Unknown command: {command}")
# Match with multiple values (| means OR)
status_code = 404
match status_code:
case 200 | 201 | 204:
print("Success")
case 301 | 302:
print("Redirect")
case 400:
print("Bad Request")
case 404:
print("Not Found")
case 500 | 503:
print("Server Error")
case _:
print(f"Unknown status: {status_code}")
# Match with guard (if condition)
x = 15
match x:
case n if n < 0:
print("negative")
case 0:
print("zero")
case n if n < 10:
print("small positive")
case n if n < 100:
print(f"medium positive: {n}") # n is bound here
case _:
print("large")
# Structural matching - match data structure shapes
point = (0, 5)
match point:
case (0, 0):
print("Origin")
case (x, 0):
print(f"On x-axis at {x}")
case (0, y):
print(f"On y-axis at {y}")
case (x, y):
print(f"Point at ({x}, {y})")
# Match dictionaries
event = {"type": "click", "button": "left", "x": 100, "y": 200}
match event:
case {"type": "click", "button": "left"}:
print("Left click")
case {"type": "click", "button": "right"}:
print("Right click")
case {"type": "keydown", "key": key}:
print(f"Key pressed: {key}")
case _:
print("Other event")
# Match with class patterns
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p = Point(0, 5)
match p:
case Point(x=0, y=0):
print("Origin")
case Point(x=0, y=y):
print(f"On y-axis at y={y}")
case Point(x=x, y=0):
print(f"On x-axis at x={x}")
case Point(x=x, y=y):
print(f"Point({x}, {y})")
pass and ...
Python requires at least one statement in every block. Use pass or ... as no-op placeholders.
# pass - traditional empty block placeholder
if condition := True:
pass # TODO: implement later
# Pass in empty function
def placeholder():
pass # function that does nothing (yet)
# Pass in empty class
class EmptyClass:
pass
# ... (Ellipsis) - increasingly common alternative
def not_implemented_yet():
...
class AbstractBase:
def process(self, data):
... # subclasses must implement this
# Ellipsis is commonly used in type stubs and Protocol
from typing import Protocol
class Readable(Protocol):
def read(self, size: int = -1) -> bytes: ...
def close(self) -> None: ...
# pass vs ... - functionally identical for placeholders
# Use pass for: intentionally empty loops/branches
for _ in range(5):
pass # consuming the iterator without doing anything
# Use ... for: "to be implemented", abstract methods, type annotations
Short-Circuit Patterns
Python's logical operators and and or short-circuit - they stop evaluating as soon as the result is determined.
# 'and' stops at first falsy, 'or' stops at first truthy
# Safe attribute/key access
user = {"name": "Alice", "address": None}
# Without short-circuit - might fail
# city = user["address"]["city"] # AttributeError: 'NoneType' has no ...
# With 'and' - safe chaining
city = user.get("address") and user["address"].get("city")
print(city) # None (address is None, stops there)
# Default values
settings = {}
timeout = settings.get("timeout") or 30 # 30 if not set
retries = settings.get("retries") or 3 # 3 if not set
# Call function only if object exists
def send_email(user):
print(f"Sending to {user}")
user = None
user and send_email(user) # send_email never called
user = "alice@example.com"
user and send_email(user) # sends email
# Guard pattern - early return
def process(data):
if not data:
return None # early exit for empty input
if not isinstance(data, list):
return None
return [x * 2 for x in data]
# Dict dispatch instead of long if/elif chains
def add(a, b): return a + b
def sub(a, b): return a - b
def mul(a, b): return a * b
operations = {"+": add, "-": sub, "*": mul}
op = "+"
if op in operations:
result = operations[op](10, 5)
print(result) # 15