Control flow is how your program makes decisions. Python uses indentation rather than braces to group statements, and it offers if, elif, else for branching plus the match statement added in 3.10 for structural pattern matching. This lesson covers every form of conditional in Python with examples you can run.
The if, elif, else chain
An if statement tests a boolean expression. If the expression is truthy, the indented block runs. Otherwise Python checks the next elif branch. The optional else runs when no branch matched. Indentation is part of the syntax. Four spaces is the standard.
score = 87
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"Score {score} is a {grade}") # Score 87 is a B
Truthiness and falsy values
Python does not require a strict boolean. Any object can be tested for truth. The falsy values are False, None, zero of any numeric type, empty sequences and collections, and objects whose __bool__ returns False. Everything else is truthy.
def describe(value):
if value:
return "truthy"
return "falsy"
print(describe(0)) # falsy
print(describe("")) # falsy
print(describe([])) # falsy
print(describe(None)) # falsy
print(describe(0.0)) # falsy
print(describe("hello")) # truthy
print(describe([0])) # truthy (one element)
The ternary expression
For short conditionals, Python offers a one-line ternary. It is an expression, so it returns a value. The order is value_if_true if condition else value_if_false, which reads naturally in English.
age = 19
status = "adult" if age >= 18 else "minor"
print(status) # adult
# Equivalent if-else block
if age >= 18:
status = "adult"
else:
status = "minor"
The match statement (Python 3.10+)
match is structural pattern matching. It compares a subject value against a series of patterns and runs the body of the first matching pattern. Use it when you find yourself writing long if-elif chains on the same value.
def http_status(code: int) -> str:
match code:
case 200:
return "OK"
case 301 | 302:
return "Redirect"
case 404:
return "Not Found"
case 500:
return "Server Error"
case _:
return "Unknown"
print(http_status(404)) # Not Found
Capturing values with patterns
Patterns can capture parts of the subject. The as keyword binds a captured name. Sequence patterns match lists and tuples. Mapping patterns match dicts. Class patterns match objects by type and position.
def describe_command(cmd):
match cmd:
case ["quit"]:
return "exit the program"
case ["move", direction, steps]:
return f"move {steps} steps {direction}"
case ["speak", *words]:
return f"say: {' '.join(words)}"
case _:
return "unknown command"
print(describe_command(["move", "north", 3])) # move 3 steps north
print(describe_command(["speak", "hello", "world"])) # say: hello world
Guards for extra conditions
Add an if clause to a case to add an extra condition. The pattern must match and the guard must be true for the case to fire. Guards let you match a shape and a value at the same time.
def classify_temperature(celsius: int) -> str:
match celsius:
case x if x < 0:
return "freezing"
case x if x < 20:
return "cold"
case x if x < 30:
return "warm"
case _:
return "hot"
print(classify_temperature(15)) # cold
Common pitfalls in conditionals
Three mistakes appear over and over in beginner code. The first is using one equals sign where you meant two. Writing if x = 5: is a SyntaxError, but the related mistake if x == None: is allowed and silently works for most objects — yet it catches None and other singletons only because Python interns them. The PEP 8 rule is explicit: use if x is None. The second pitfall is short-circuit surprises. In if user and user.is_admin: the second clause is skipped when user is falsy, which is usually what you want, but in if user.is_admin and user: you crash with AttributeError when user is None. Order matters. The third is the empty-match wildcard. match x: case _: matches anything and shadows the variable, so any code that expects to use x later will see the wildcard capture. Use a real name or add a guard to keep your intent clear.
How match compares to switch in JavaScript and C++
Python's match looks like JavaScript's switch but is much more powerful. JavaScript switch falls through by default and requires an explicit break; Python's match never falls through, executes only the first matching case, and supports destructuring patterns. C++ switch case on integers is essentially a jump table; match in Python is more like a pattern engine. If you are coming from either language, the key mental shift is this: switch/case asks "is this value equal to that constant?" while match asks "does this structure look like that shape?". That difference is why match cases can bind names (case ["move", direction, steps]:) where a switch case can only compare against literal values.
Best practices for readable conditionals
Two habits keep condition-heavy code maintainable. First, prefer early returns over deep nesting. Instead of a ten-level if-elif chain inside one function, return at the top of each branch and keep the main logic flat. Second, give boolean conditions descriptive names. if not user.has_active_subscription(): reads better than if not user.sub and not user.trial and not user.lifetime_pass. Pulling the rule into a property on the class lets the compiler inline it and lets your tests exercise the rule in isolation. Apply them together and conditionals stop being the longest paragraph in your file.
Summary
Use if for branching, ternary expressions for short inline choices, and match when you compare a single value against many shapes. The next lesson covers loops, the for and while statements, and the loop-else clause that surprises newcomers.