Modern Python Basics #3: Control flow — if, while, for, match-case

4 min read

Building on the variables from #2 Variables, basic types, and type hints, this time we make flowif, while, for, and the match-case introduced in 3.10.

Two big features of Python’s flow control:

  1. Blocks are delimited by indentation. No braces {}.
  2. Headers end with a colon (:). Like if x > 0:.

So indentation is grammar. Conventionally four spaces (PEP 8). Mixing tabs and spaces is an error.

if / elif / else #

Most similar to other languages. The difference is no parentheses.

if basics
score = 85

if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
else:
    grade = "F"

print(grade)   # B

Not else ifelif. The historical reason doesn’t matter much; just memorize it.

One-liner — ternary operator #

Different from C-style cond ? a : b. Python uses a if cond else b.

Ternary
status = "성인" if age >= 19 else "미성년"

The natural-language order feels strange at first but reads well once familiar.

Truthiness in conditions — using falsy #

We continue using the falsy values from #2.

falsy condition
items: list[int] = []

if not items:
    print("비어 있음")    # this prints

# Same meaning below, but the above is more Pythonic
if len(items) == 0:
    print("비어 있음")

For empty-list checks, len(x) == 0 is usually shortened to not x.

is vs == #

This needs care.

is vs ==
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)   # True   (same value)
print(a is b)   # False  (different objects)

x = None
print(x is None)   # True  ← always use `is` for None

== compares values; is compares object identity. Always compare None, True, False with is. Use == for everything else.

while #

Repeat while a condition is true.

while basics
count = 0
while count < 5:
    print(count)
    count += 1

There’s no ++. It’s count += 1.

break, continue #

break / continue
n = 0
while True:
    n += 1
    if n % 2 == 0:
        continue       # skip even
    if n > 7:
        break          # stop after 7
    print(n)
# 1, 3, 5, 7

while True + break is a common infinite-loop pattern when the condition is too complex to put up top.

The else clause — a lesser-known feature #

while and for can have an else. It runs “when the loop ended without a break.”

while-else
n = 0
while n < 10:
    if n == 99:
        break
    n += 1
else:
    print("break 없이 끝났음")    # this prints

Not common, but a clean fit for “didn’t find what we were looking for after iterating everything.” Overuse hurts readability — use sparingly.

for — always iterate sequences #

There is no C-style for (i = 0; i < n; i++). Python’s for always iterates an iterable.

for basics
names = ["a", "b", "c"]
for name in names:
    print(name)

Strings are iterable too.

One character at a time
for ch in "hello":
    print(ch)
# h, e, l, l, o

range — when you need a numeric sequence #

range
for i in range(5):       # 0, 1, 2, 3, 4
    print(i)

for i in range(2, 7):    # 2, 3, 4, 5, 6
    print(i)

for i in range(0, 10, 2):  # 0, 2, 4, 6, 8 (step 2)
    print(i)

It’s range(start, stop, step). stop is excluded (half-open interval). Confusing at first, but it matches the slice convention from other languages, so it sticks soon.

enumerate — when you need the index #

When you need indices
items = ["사과", "배", "감"]
for i, item in enumerate(items):
    print(f"{i}: {item}")
# 0: 사과
# 1: 배
# 2: 감

for i in range(len(items)) then items[i] is an antipattern. enumerate in one word.

zip — iterate multiple lists in parallel #

zip
names = ["커티스", "스미스", "존"]
ages = [30, 25, 40]

for name, age in zip(names, ages):
    print(f"{name}: {age}")

If lengths differ, it stops at the shorter one. To raise an error on mismatched lengths, zip(..., strict=True) (3.10+).

strict mode
for n, a in zip(names, ages, strict=True):
    ...

Pattern matching — match-case (3.10+) #

It looks like switch, but does much more. Python designed it not as simple branching but as true pattern matching.

The simplest form #

Simple branching
def http_status(code: int) -> str:
    match code:
        case 200:
            return "OK"
        case 404:
            return "Not Found"
        case 500:
            return "Server Error"
        case _:
            return "Unknown"

case _: is the wildcard (default). It’s where JavaScript’s default: would go.

Multiple values per case — | #

OR pattern
def category(code: int) -> str:
    match code:
        case 200 | 201 | 204:
            return "성공"
        case 400 | 401 | 403 | 404:
            return "클라이언트 오류"
        case 500 | 502 | 503:
            return "서버 오류"
        case _:
            return "기타"

Destructuring — the real power starts here #

You can use the shape itself of lists, tuples, and dicts as a pattern.

Destructuring
def describe(point: tuple[int, ...]) -> str:
    match point:
        case ():
            return "빈 점"
        case (x,):
            return f"1D: {x}"
        case (x, y):
            return f"2D: ({x}, {y})"
        case (x, y, z):
            return f"3D: ({x}, {y}, {z})"
        case _:
            return "고차원"

print(describe((1, 2)))      # 2D: (1, 2)
print(describe((1, 2, 3)))   # 3D: (1, 2, 3)

x and y inside each pattern bind values at that position to variables. Variables are created at the same time as matching. Feels like JavaScript destructuring + switch combined.

Dictionary patterns #

dict pattern
event = {"type": "click", "x": 100, "y": 200}

match event:
    case {"type": "click", "x": x, "y": y}:
        print(f"클릭: ({x}, {y})")
    case {"type": "key", "code": code}:
        print(f"키: {code}")
    case _:
        print("알 수 없는 이벤트")

Pattern: certain keys exist and their values are bound to variables. Fits API response handling really well.

Guards — adding if conditions #

Guards
match (x, y):
    case (a, b) if a == b:
        print(f"동일: {a}")
    case (a, b) if a > b:
        print(f"a 가 큼")
    case _:
        print("그 외")

Class patterns #

Class matching
class Circle:
    def __init__(self, radius: float):
        self.radius = radius

class Square:
    def __init__(self, side: float):
        self.side = side

shape = Circle(5)

match shape:
    case Circle(radius=r):
        print(f"원, 반지름 {r}")
    case Square(side=s):
        print(f"정사각형, 한 변 {s}")

Type check + attribute extraction in one line.

Why it’s different from switch #

match-case isn’t simple multi-way branching but a tool for branching by data shape. Rewriting the code above with if-elif gets long and awkward.

Same job without match
if isinstance(shape, Circle):
    r = shape.radius
    print(f"원, 반지름 {r}")
elif isinstance(shape, Square):
    s = shape.side
    print(f"정사각형, 한 변 {s}")

Same behavior, but type checking and attribute extraction are separate. match collapses the two into a single line. Highly useful for handling JSON/event objects.

A truth about flow — pass #

A block can’t be empty. Without an indented block, it’s a syntax error. To leave a placeholder, use pass.

Placeholder
def todo(): 
    pass    # implement later

if x > 0:
    pass    # ignore for now
else:
    handle(x)

You’ll also see ... (Ellipsis) in the same place, but pass is more idiomatic.

for also has a pattern called comprehensions #

There’s syntax that collapses a for loop into a single line when building lists, dicts, and sets.

Quick preview
squares = [x ** 2 for x in range(5)]
# [0, 1, 4, 9, 16]

We cover this in earnest in the next post (#4 Collections and comprehensions).

Wrap-up #

What this post covered:

  • Blocks via indentation — 4 spaces, headers end with a colon
  • if / elif / else, ternary a if cond else b
  • is for object identity, == for value; check None with is None
  • while with break / continue / else
  • for in iterates iterables — range, enumerate, zip
  • match-case — simple branching, OR pattern, destructuring, dict pattern, guard, class matching
  • pass for empty blocks

In the next post (#4 Collections and comprehensions) we cover the four most-used data structures — list, tuple, dict, set — and comprehensions that build them in a single line.

X