ZetCode

Python control flow

last modified October 30, 2026 Python 3.15

In this article we show how to control the flow of a Python program.

Control flow

When a Python program is run, the code is executed from top to bottom. The flow of the program can be altered with various keywords, including if/elif/else, for, while, break, continue, match, and try.

The control flow structures are used to execute code conditionally or repeatedly. A block of code that belongs to a statement is defined by its indentation; there are no braces.

The if statement

The if keyword is followed by an expression and a colon. If the expression is true, the indented block below it is executed.

if_statement.py
temperatures = [12, -3, 25, 0]

for t in temperatures:
    if t > 20:
        print(f'{t} °C: warm')
        print('  take sunglasses')

print('done')

We go over a list of temperatures and print a message for the warm ones.

if t > 20:
    print(f'{t} °C: warm')
    print('  take sunglasses')

Both indented lines form the block of the if statement. For temperatures that are not above 20, nothing is done. The final print is not indented under the loop, so it runs once at the end.

$ python if_statement.py
25 °C: warm
  take sunglasses
done

The else clause

The else clause creates a second branch. If the expression after if is false, the block after else is executed.

if_else.py
for n in [7, 10, -4, 0]:
    if n % 2 == 0:
        print(f'{n} is even')
    else:
        print(f'{n} is odd')

Exactly one of the two blocks runs for each number. The remainder operator % tells us whether a number is even.

$ python if_else.py
7 is odd
10 is even
-4 is even
0 is even

The elif clause

We create multiple branches with the elif keyword. Each condition is tested only if all previous conditions were false. The optional else block runs when none of the conditions match.

elif_chain.py
def grade(score):
    if score >= 90:
        return 'A'
    elif score >= 80:
        return 'B'
    elif score >= 70:
        return 'C'
    elif score >= 60:
        return 'D'
    else:
        return 'F'

for score in [95, 83, 70, 61, 42]:
    print(score, grade(score))

The grade function turns a score into a letter grade.

if score >= 90:
    return 'A'
elif score >= 80:
    return 'B'

The order of the tests matters. A score of 95 also satisfies score >= 80, but the first true condition wins and the rest are skipped. That is why the second test does not need an upper bound.

$ python elif_chain.py
95 A
83 B
70 C
61 D
42 F

Conditional expressions

A conditional expression, sometimes called the ternary operator, chooses between two values in a single expression: x if condition else y.

conditional_expr.py
age = 17

status = 'adult' if age >= 18 else 'minor'
print(status)

items = 1
print(f'{items} item{"" if items == 1 else "s"}')

values = [3, -1, 4, -5]
print([v if v > 0 else 0 for v in values])

Conditional expressions are handy in assignments, f-strings, and comprehensions.

print(f'{items} item{"" if items == 1 else "s"}')

We choose between the singular and plural form of a word.

print([v if v > 0 else 0 for v in values])

In a comprehension, a conditional expression transforms each element; here, negative values are replaced with zero.

$ python conditional_expr.py
minor
1 item
[3, 0, 4, 0]

Truthiness

The condition does not have to be a boolean. Every Python object is either truthy or falsy. Zero numbers, empty containers and strings, None, and False are falsy; everything else is truthy.

truthiness.py
values = [0, 0.0, '', [], {}, None, False, 1, 'no', [0], ' ']

for val in values:
    if val:
        print(f'{val!r:>6} is truthy')
    else:
        print(f'{val!r:>6} is falsy')

We test a list of values directly in an if statement.

if val:

This is the idiomatic way to check for an empty list or string; there is no need to write if len(val) > 0. Note that [0] and ' ' are truthy, because they are not empty.

$ python truthiness.py
     0 is falsy
   0.0 is falsy
    '' is falsy
    [] is falsy
    {} is falsy
  None is falsy
 False is falsy
     1 is truthy
  'no' is truthy
   [0] is truthy
   ' ' is truthy

Boolean operators and chained comparisons

Conditions are combined with and, or, and not. The and and or operators short-circuit: the right operand is evaluated only when it is needed.

boolean_ops.py
age = 34

if 18 <= age < 65:
    print('working age')

def check(name, result):
    print(f'checking {name}')
    return result

if check('a', False) and check('b', True):
    print('both')

if check('c', True) or check('d', True):
    print('at least one')

name = '' or 'guest'
print(name)

if not []:
    print('empty list')

The check function prints its name, so we can see which operands are evaluated.

if 18 <= age < 65:

Comparisons can be chained. The expression means 18 <= age and age < 65.

if check('a', False) and check('b', True):

Because the left operand of and is false, the whole expression is false and check('b') is never called. Likewise, the true left operand of or makes check('d') unnecessary.

name = '' or 'guest'

The or operator returns one of its operands, not True or False. This gives a short way to provide a default for an empty value.

$ python boolean_ops.py
working age
checking a
checking c
at least one
guest
empty list

The while loop

The while keyword creates a loop. The block is executed again and again as long as the condition is true.

while_loop.py
numbers = [22, 34, 12, 32, 4]
total = 0
i = 0

while i < len(numbers):
    total += numbers[i]
    i += 1

print('The sum is:', total)

n = 27
steps = 0

while n != 1:
    n = n // 2 if n % 2 == 0 else 3 * n + 1
    steps += 1

print('Collatz steps for 27:', steps)

The example contains two while loops.

while i < len(numbers):
    total += numbers[i]
    i += 1

The first loop sums the numbers of a list with an index variable. A for loop or the sum built-in would be simpler here; while is the right tool when we do not know the number of iterations in advance.

while n != 1:
    n = n // 2 if n % 2 == 0 else 3 * n + 1
    steps += 1

The second loop counts the steps of the Collatz sequence, whose length cannot be computed beforehand.

$ python while_loop.py
The sum is: 104
Collatz steps for 27: 111

The break statement

The break statement terminates the innermost loop immediately.

break_loop.py
import random

while True:
    val = random.randint(1, 30)
    print(val, end=' ')

    if val == 22:
        break

print()

We print random integers until we get 22. The while True loop would run forever; break is the only way out. The output differs from run to run.

$ python break_loop.py
6 1 3 24 26 26 26 6 30 17 30 17 10 27 10 1 11 7 21 15 4 1 13 11 24 5 20 8 24 24 11 2 16 24 17 16 28 7 8 26 16 26 20 11 6 22 

The continue statement

The continue statement skips the rest of the current iteration and starts the next one.

continue_loop.py
num = 0

while num < 30:
    num += 1

    if num % 3 == 0:
        continue

    print(num, end=' ')

print()

lines = ['host = localhost', '', '# comment', 'port = 8080']

for line in lines:
    if not line or line.startswith('#'):
        continue
    key, value = line.split(' = ')
    print(key, '->', value)

The first loop prints numbers up to 30 that are not divisible by three.

for line in lines:
    if not line or line.startswith('#'):
        continue
    key, value = line.split(' = ')

The second loop shows a common use: skipping empty lines and comments early keeps the main code of the loop unindented.

$ python continue_loop.py
1 2 4 5 7 8 10 11 13 14 16 17 19 20 22 23 25 26 28 29 
host -> localhost
port -> 8080

The for loop

The for loop iterates over the items of any iterable: strings, lists, ranges, dictionaries, files, generators, and more.

for_loop.py
for c in 'sky':
    print(c, end=' ')
print()

for i in range(2, 11, 3):
    print(i, end=' ')
print()

fruits = ['apple', 'banana', 'cherry']

for idx, fruit in enumerate(fruits, start=1):
    print(idx, fruit)

prices = [1.2, 0.5, 3.0]

for fruit, price in zip(fruits, prices):
    print(f'{fruit}: {price:.2f}')

capitals = {'Slovakia': 'Bratislava', 'Hungary': 'Budapest'}

for country, capital in capitals.items():
    print(f'{capital} is the capital of {country}')

The example shows the most common forms of the for loop.

for i in range(2, 11, 3):

The range function generates numbers from 2 up to, but not including, 11 with step 3.

for idx, fruit in enumerate(fruits, start=1):

The enumerate function gives us an index together with each item; there is no need for a counter variable.

for fruit, price in zip(fruits, prices):

The zip function walks over several iterables in parallel.

for country, capital in capitals.items():

The items method of a dictionary yields key/value pairs.

The for statement is covered in more detail in Python for loop.

$ python for_loop.py
s k y 
2 5 8 
1 apple
2 banana
3 cherry
apple: 1.20
banana: 0.50
cherry: 3.00
Bratislava is the capital of Slovakia
Budapest is the capital of Hungary

The else clause of loops

Both for and while loops can have an else clause. It runs when the loop finishes normally, but not when it is terminated with break. It is best read as "no break".

loop_else.py
def find_divisor(n):
    for d in range(2, int(n ** 0.5) + 1):
        if n % d == 0:
            print(f'{n} is divisible by {d}')
            break
    else:
        print(f'{n} is prime')

for n in [91, 97, 1001, 7919]:
    find_divisor(n)

def connect(replies, limit=3):
    attempt = 0
    while attempt < limit:
        reply = replies[attempt]
        attempt += 1
        if reply == 'ok':
            print(f'connected after {attempt} attempts')
            break
    else:
        print(f'gave up after {limit} attempts')

connect(['timeout', 'ok'])
connect(['timeout', 'timeout', 'refused'])

We use the clause in two typical search situations.

for d in range(2, int(n ** 0.5) + 1):
    if n % d == 0:
        print(f'{n} is divisible by {d}')
        break
else:
    print(f'{n} is prime')

We look for a divisor. If one is found, the loop breaks. If the loop runs out of candidates, the else block reports a prime. Note that the else is aligned with for, not with if.

while attempt < limit:
    ...
    if reply == 'ok':
        print(f'connected after {attempt} attempts')
        break
else:
    print(f'gave up after {limit} attempts')

The while version retries an operation. The else block handles the case where all attempts failed, without an extra flag variable.

$ python loop_else.py
91 is divisible by 7
97 is prime
1001 is divisible by 7
7919 is prime
connected after 2 attempts
gave up after 3 attempts

Leaving nested loops

The break statement leaves only the innermost loop. To leave several loops at once, we can put them in a function and use return.

nested_break.py
grid = [
    [1, 4, 7],
    [2, 9, 3],
    [8, 5, 6],
]

def find(grid, target):
    for r, row in enumerate(grid):
        for c, val in enumerate(row):
            if val == target:
                return r, c
    return None

print(find(grid, 9))
print(find(grid, 10))

found = None

for r, row in enumerate(grid):
    for c, val in enumerate(row):
        if val == 9:
            found = (r, c)
            break
    if found:
        break

print(found)

We search a two-dimensional grid for a value.

for r, row in enumerate(grid):
    for c, val in enumerate(row):
        if val == target:
            return r, c
return None

The return statement ends both loops and the function. If the loops finish without a match, the function returns None.

    if found:
        break

Without a function, we need a variable that the outer loop checks after the inner loop ends. The function version is shorter and clearer.

$ python nested_break.py
(1, 1)
None
(1, 1)

The walrus operator

The assignment expression :=, called the walrus operator, assigns a value and returns it in one expression. It is often used in loop and if conditions.

walrus.py
import io

stream = io.BytesIO(b'abcdefghijklmnopqrstuvw')

while chunk := stream.read(5):
    print(chunk)

data = [4, 18, 7, 25, 3]

if (n := len(data)) > 3:
    print(f'list is too long ({n} elements)')

print([y for x in data if (y := x * 2) > 10])

We read a stream in chunks and reuse computed values.

while chunk := stream.read(5):
    print(chunk)

We read five bytes, store them in chunk, and test them in one step. The loop stops when read returns an empty, falsy bytes object at the end of the stream.

if (n := len(data)) > 3:

The length is computed once and is available inside the block. The parentheses are needed because := binds less tightly than >.

$ python walrus.py
b'abcde'
b'fghij'
b'klmno'
b'pqrst'
b'uvw'
list is too long (5 elements)
[36, 14, 50]

The pass statement

A block cannot be empty. The pass statement does nothing and serves as a placeholder where a statement is syntactically required.

pass_stmt.py
class NotReady(Exception):
    pass

def todo():
    pass

for n in range(3):
    pass

print(todo())
print(n)

try:
    raise NotReady('later')
except NotReady:
    pass

print('finished')

We use pass in a class, a function, a loop, and an exception handler.

try:
    raise NotReady('later')
except NotReady:
    pass

An empty except block silences an exception. Do this only for exceptions that are truly expected; silencing all errors hides bugs.

$ python pass_stmt.py
None
2
finished

The match statement

The match statement compares a value against a series of patterns and executes the block of the first matching case.

match_stmt.py
grades = ['A', 'B', 'C', 'D', 'E', 'F', 'FX']

for grade in grades:
    match grade:
        case 'A' | 'B' | 'C' | 'D' | 'E':
            print(grade, 'passed')
        case 'F' | 'FX':
            print(grade, 'failed')

We have a list of grades. Grades A to E pass the exam, while F and FX fail it.

match grade:
    case 'A' | 'B' | 'C' | 'D' | 'E':
        print(grade, 'passed')
    case 'F' | 'FX':
        print(grade, 'failed')

The | operator combines several alternatives in one pattern. Unlike the switch statement of C-like languages, there is no fall-through and no break is needed.

$ python match_stmt.py
A passed
B passed
C passed
D passed
E passed
F failed
FX failed

Matching structure

The real strength of match is matching the shape of data and binding parts of it to variables.

match_patterns.py
def handle(command):
    match command.split():
        case ['go', direction] if direction in ('north', 'south', 'east', 'west'):
            return f'moving {direction}'
        case ['go', _]:
            return 'unknown direction'
        case ['take', *items] if items:
            return f'taking {", ".join(items)}'
        case ['quit' | 'exit']:
            return 'bye'
        case []:
            return 'say something'
        case _:
            return f'unknown command: {command!r}'

for cmd in ['go north', 'go up', 'take key lamp', 'exit', '', 'dance']:
    print(f'{cmd!r:16} -> {handle(cmd)}')

We parse commands of a text adventure game.

case ['go', direction] if direction in ('north', 'south', 'east', 'west'):

The pattern matches a list of exactly two words, the first being go. The second word is bound to direction. The if guard adds an extra condition.

case ['take', *items] if items:

A starred name collects the remaining elements into a list.

case ['quit' | 'exit']:
...
case _:

Alternatives can be nested inside a pattern. The wildcard _ matches anything and acts as the default branch.

Pattern matching is covered in more detail in Python pattern match.

$ python match_patterns.py
'go north'       -> moving north
'go up'          -> unknown direction
'take key lamp'  -> taking key, lamp
'exit'           -> bye
''               -> say something
'dance'          -> unknown command: 'dance'

try, except, else, and finally

Exceptions also change the flow of a program. The try statement has four parts: the guarded code, handlers for exceptions, an else block that runs when no exception occurred, and a finally block that runs in every case.

try_flow.py
def parse(text):
    try:
        value = int(text)
    except ValueError:
        print(f'{text!r}: not a number')
        return None
    else:
        print(f'{text!r}: parsed {value}')
        return value
    finally:
        print('  cleanup')

parse('42')
parse('forty')

The parse function converts text to an integer.

else:
    print(f'{text!r}: parsed {value}')
    return value
finally:
    print('  cleanup')

The else block keeps the success path out of the try block, so that exceptions raised there are not caught by accident. The finally block runs even though both branches leave the function with return.

$ python try_flow.py
'42': parsed 42
  cleanup
'forty': not a number
  cleanup

except without parentheses

Since Python 3.14 (PEP 758), several exception types in an except clause can be listed without parentheses, as long as there is no as clause.

except_comma.py
def convert(value):
    try:
        return int(value)
    except ValueError, TypeError:
        return None

print(convert('12'))
print(convert('x'))
print(convert(None))

The convert function returns None for values that cannot be converted.

except ValueError, TypeError:

int('x') raises a ValueError and int(None) a TypeError; both are handled. Older versions reject this syntax, as the run with Python 3.13 shows.

$ python except_comma.py
12
None
None
$ python3.13 except_comma.py
  File "/home/user/python/except_comma.py", line 4
    except ValueError, TypeError:
           ^^^^^^^^^^^^^^^^^^^^^
SyntaxError: multiple exception types must be parenthesized

return, break, and continue in finally

A return, break, or continue inside a finally block silently discards any exception that is being raised. Since Python 3.14 (PEP 765), the compiler emits a SyntaxWarning for such code.

finally_return.py
def risky():
    try:
        raise ValueError('important error')
    finally:
        return 'finally wins'

print(risky())

The ValueError is lost, because the return in the finally block replaces it with a normal return value.

finally:
    return 'finally wins'

The program prints the same result with all versions, but Python 3.14 and 3.15 warn about the hidden bug. Python 3.13 runs it silently.

$ python finally_return.py
/home/user/python/finally_return.py:5: SyntaxWarning: 'return' in a 'finally' block
  return 'finally wins'
finally wins
$ python3.14 finally_return.py
/home/user/python/finally_return.py:5: SyntaxWarning: 'return' in a 'finally' block
  return 'finally wins'
finally wins
$ python3.13 finally_return.py
finally wins

Source

More Control Flow Tools - Python tutorial

In this article we have worked with program control flow in Python.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.