Skip to main content

🔀 Python Control Structures and Functions

Data on its own just sits there. Control structures let your program make decisions and repeat work, and functions let you package that logic into reusable, testable units. Together they are the engine of every backend script you'll write.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Make decisions with if / elif / else and conditional expressions
  • Repeat work with for and while loops, and steer them with break, continue, and else
  • Define functions with parameters, default values, and return values
  • Accept flexible arguments using *args and **kwargs
  • Combine control flow and functions into a small analytical program

Estimated Time: 35–45 minutes  •  Difficulty: Beginner

Hands-on: Write and test a classify_temperatures() analysis function.

In This Lesson

Why Control Flow?

Control structures are the traffic signals of a program. They direct the flow of execution — deciding which code runs, when it runs, and how many times. Without them, a program could only march straight from the first line to the last, doing exactly the same thing every run.

Python implements these ideas with unusually clean syntax, staying true to its readability-first philosophy. Everything you learn here transfers directly to writing API request handlers, processing database rows, and validating user input later in this course.

📖 The two big ideas

Selection — choosing a path based on a condition (if/elif/else).

Iteration — repeating a block until some condition is met (for/while).

Conditional Statements

Conditionals let a program branch, just as we make decisions all day: if it's raining, take an umbrella.

if, if-else, if-elif-else

# Simple if
temperature = 28
if temperature > 25:
    print("It's a hot day! Stay hydrated.")

# Two alternatives with else
age = 17
if age >= 18:
    print("You are eligible to vote.")
else:
    print(f"Wait {18 - age} more years.")

# Many alternatives with elif
score = 85
if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
else:
    grade = "F"
print(f"Your grade is {grade}")

Python evaluates each condition top to bottom and runs the first branch that is true, then skips the rest. Think of a customer-service phone menu routing each caller to exactly one department.

flowchart TD A[Start] --> B{score >= 90?} B -->|Yes| C[grade = A] B -->|No| D{score >= 80?} D -->|Yes| E[grade = B] D -->|No| F{score >= 70?} F -->|Yes| G[grade = C] F -->|No| H[grade = F] C --> Z[End] E --> Z G --> Z H --> Z

Conditional expressions (the ternary)

For a simple either/or assignment, Python offers a compact one-liner:

age = 20
status = "adult" if age >= 18 else "minor"
print(status)   # adult

⚠️ Comparison vs assignment

= assigns a value; == compares two values. Writing if x = 5: is a syntax error in Python (a small mercy that catches a classic bug). Use == inside conditions.

Loops

Loops repeat a block of code — the assembly line of programming, processing items one after another.

for loops iterate over sequences

fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(f"I like {fruit}.")

# enumerate() gives you the index too
for i, fruit in enumerate(fruits):
    print(f"{i}: {fruit}")

# Iterate a dict's key-value pairs
person = {"name": "Helen", "city": "Boston"}
for key, value in person.items():
    print(f"{key}: {value}")

range() generates numbers

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

for i in range(1, 6):       # 1,2,3,4,5  (start, stop)
    print(i)

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

while loops repeat until a condition ends

A while loop runs as long as its condition stays true — ideal when you don't know the number of iterations in advance, such as retrying a network call until it succeeds.

countdown = 5
while countdown > 0:
    print(f"{countdown}...")
    countdown -= 1
print("Blast off!")

⚠️ The infinite loop

A while loop whose condition never becomes false runs forever. Always make sure something inside the loop moves it toward the exit — here, countdown -= 1. Forget it and your server hangs.

Loop Control & else

Three keywords let you steer a loop's flow:

  • break — exit the loop immediately (an emergency exit door).
  • continue — skip to the next iteration (a "staff only" door back to the start).
  • pass — do nothing; a placeholder where syntax requires a statement.
# Stop at the first number divisible by 7
for num in range(1, 100):
    if num % 7 == 0:
        print(f"Found it! {num} is divisible by 7.")
        break

# Skip even numbers, print only odds
for num in range(10):
    if num % 2 == 0:
        continue
    print(f"{num} is odd")

📖 The loop else clause

Python loops can have an else block that runs only if the loop finished without hitting break. It's perfect for search-and-report patterns:

target = 42
for n in [10, 20, 30]:
    if n == target:
        print("Found target!")
        break
else:
    print("Target not found.")   # runs — no break happened
flowchart TD A[Start loop] --> B{More items?} B -->|No| C[else block runs] B -->|Yes| D{Condition met?} D -->|Yes| E[break — skip else] D -->|No| F[continue to next] F --> B C --> G[End] E --> G

Defining Functions

A function is a named, reusable block of code that performs one task — a specialized tool in your kit. Functions keep code DRY (Don't Repeat Yourself), testable, and easy to read.

def, parameters, and return

def greet(name):
    """Return a friendly greeting for the given name."""
    return f"Hello, {name}! How are you today?"

message = greet("Alice")   # call the function
print(message)

The triple-quoted docstring documents what the function does; tools and help() read it. A function that hits return sends a value back to the caller. A function with no return (or a bare return) hands back None.

Default parameter values

def make_coffee(size="medium", kind="regular", milk=True):
    """Prepare a coffee order, filling in sensible defaults."""
    order = f"{size} {kind} coffee"
    if milk:
        order += " with milk"
    return order + "."

print(make_coffee())                    # medium regular coffee with milk.
print(make_coffee("large"))             # override just the size
print(make_coffee(kind="espresso", milk=False))  # keyword arguments

Passing arguments by keyword (kind="espresso") makes calls self-documenting and lets you skip earlier optional parameters.

⚠️ The mutable default trap

Never use a mutable object like [] or {} as a default value — it's created once and shared across all calls. Use None as the sentinel instead:

# Buggy: the list persists between calls
def add_item(item, cart=[]):   # DON'T
    cart.append(item)
    return cart

# Correct
def add_item(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart

Flexible Arguments

Sometimes a function should accept any number of arguments. Python provides *args (extra positional arguments, collected into a tuple) and **kwargs (extra keyword arguments, collected into a dict).

def total(*numbers):
    """Sum any number of positional arguments."""
    return sum(numbers)

print(total(1, 2, 3))        # 6
print(total(5, 10, 15, 20))  # 50

def build_profile(name, **details):
    """Accept a name plus arbitrary extra key-value fields."""
    profile = {"name": name}
    profile.update(details)
    return profile

print(build_profile("Alice", age=30, city="NYC"))
# {'name': 'Alice', 'age': 30, 'city': 'NYC'}

You'll see *args, **kwargs constantly in web frameworks, where a handler needs to pass along whatever arguments it received without knowing them all in advance.

💡 Scope in one sentence

Variables created inside a function are local — they vanish when the function returns and don't leak out. To read an outer variable, that's fine; to reassign a module-level one, you'd need the global keyword (rarely a good idea — prefer returning values instead).

Worked Example

Let's combine conditionals, a loop, and a function into one realistic utility — the kind of analytical helper you'd find in a weather app or an IoT data pipeline.

def analyze_temperatures(readings):
    """
    Summarize a list of Celsius temperature readings.

    Args:
        readings (list[float]): temperature values in Celsius.

    Returns:
        dict: min, max, average, count, and a per-reading classification.
    """
    if not readings:                     # guard against empty input
        return {"error": "No temperature data provided"}

    result = {
        "min": min(readings),
        "max": max(readings),
        "average": sum(readings) / len(readings),
        "count": len(readings),
        "classifications": [],
    }

    for temp in readings:                # loop + if-elif chain
        if temp < 0:
            label = "freezing"
        elif temp < 10:
            label = "cold"
        elif temp < 20:
            label = "cool"
        elif temp < 30:
            label = "warm"
        else:
            label = "hot"
        result["classifications"].append(label)

    return result


weekly = [14, 21, 25, 18, 12, 22, 27]
report = analyze_temperatures(weekly)
print(f"Min: {report['min']}°C, Max: {report['max']}°C")
print(f"Average: {report['average']:.1f}°C")
print(f"Labels: {report['classifications']}")

Output

Min: 12°C, Max: 27°C
Average: 19.9°C
Labels: ['cool', 'warm', 'warm', 'cool', 'cool', 'warm', 'warm']

This one function shows every idea from the lesson working together: input validation (a conditional guard), iteration (the for loop), branching (the if-elif chain), and a rich return value (a dict).

Hands-on Exercise

🏋️ FizzBuzz & a Password Checker

Objective: Practice loops, conditionals, and function design.

Part A — FizzBuzz

Write fizzbuzz(n) that prints numbers 1..n, but prints "Fizz" for multiples of 3, "Buzz" for multiples of 5, and "FizzBuzz" for multiples of both.

Part B — Password strength

Write password_strength(pw) that returns "weak", "medium", or "strong" based on how many of these it satisfies: length ≥ 8, has an uppercase letter, has a digit, has a special character.

💡 Hint

For FizzBuzz, test the both case (n % 3 == 0 and n % 5 == 0) first, or build the output string by appending. For the password checker, count how many conditions are True using any(), str.isupper(), str.isdigit(), and a membership test against a set of symbols.

✅ Sample solution
def fizzbuzz(n):
    for i in range(1, n + 1):
        if i % 15 == 0:
            print("FizzBuzz")
        elif i % 3 == 0:
            print("Fizz")
        elif i % 5 == 0:
            print("Buzz")
        else:
            print(i)


def password_strength(pw):
    checks = [
        len(pw) >= 8,
        any(c.isupper() for c in pw),
        any(c.isdigit() for c in pw),
        any(c in "!@#$%^&*()-_+" for c in pw),
    ]
    score = sum(checks)          # True counts as 1
    if score <= 1:
        return "weak"
    elif score <= 3:
        return "medium"
    return "strong"


fizzbuzz(15)
print(password_strength("Secret123!"))   # strong

🎯 Quick Quiz

Question 1: In an if / elif / elif / else chain, how many branches execute for a single evaluation?

Question 2: What does continue do inside a loop?

Question 3: Why is def f(items=[]) considered a bug-prone pattern?

Summary & Quiz

🎉 Key Takeaways

  • if / elif / else selects exactly one branch — the first whose condition is true.
  • for loops iterate over sequences; while loops repeat until a condition ends.
  • break, continue, and the loop else clause give you fine control over iteration.
  • Functions package reusable logic; use parameters, defaults, and return values — and avoid mutable defaults.
  • *args and **kwargs let a function accept any number of positional and keyword arguments.

📚 Further Reading

🚀 What's Next?

Your functions are piling up — time to organize them. The next lesson covers modules and packages: how to split code across files, import it cleanly, and tap into Python's vast standard library and PyPI ecosystem.

🎉 Logic unlocked!

Your programs can now decide, repeat, and reuse. Let's give them structure.