Control Flow

Python Functions

Learn how to define and use functions in Python including parameters, return values, scope, lambda expressions, and best practices.

Python Functions

Functions are reusable blocks of code that perform a specific task. They help you organize code, avoid repetition, and make programs easier to read and maintain.


Defining Functions

python
# Basic function definition
def greet():
    print("Hello, World!")

# Call the function
greet()  # Hello, World!

# Function with parameters
def greet_person(name):
    print(f"Hello, {name}!")

greet_person("Alice")  # Hello, Alice!

Parameters and Arguments

Positional Arguments

python
def add(a, b):
    return a + b

result = add(3, 5)
print(result)  # 8

Keyword Arguments

python
def create_user(name, age, city):
    print(f"{name}, {age}, from {city}")

# Using keyword arguments (order doesn't matter)
create_user(age=30, city="NYC", name="Alice")
# Alice, 30, from NYC

# Mix positional and keyword (positional must come first)
create_user("Bob", city="London", age=25)
# Bob, 25, from London

Default Parameters

python
def greet(name, greeting="Hello"):
    print(f"{greeting}, {name}!")

greet("Alice")              # Hello, Alice!
greet("Bob", "Good morning") # Good morning, Bob!

Warning: Never use mutable objects as default parameters:

python
# BAD - the list persists between calls!
def add_item_bad(item, items=[]):
    items.append(item)
    return items

print(add_item_bad("a"))  # ['a']
print(add_item_bad("b"))  # ['a', 'b'] - Unexpected!

# GOOD - use None and create inside
def add_item_good(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item_good("a"))  # ['a']
print(add_item_good("b"))  # ['b'] - Correct!

*args and **kwargs

*args - Variable Positional Arguments

python
def sum_all(*args):
    """Accepts any number of positional arguments."""
    print(f"Type: {type(args)}")  # <class 'tuple'>
    return sum(args)

print(sum_all(1, 2, 3))        # 6
print(sum_all(1, 2, 3, 4, 5))  # 15

# With regular parameters
def multiply(factor, *numbers):
    return [factor * n for n in numbers]

print(multiply(2, 1, 2, 3))  # [2, 4, 6]

**kwargs - Variable Keyword Arguments

python
def print_info(**kwargs):
    """Accepts any number of keyword arguments."""
    print(f"Type: {type(kwargs)}")  # <class 'dict'>
    for key, value in kwargs.items():
        print(f"  {key}: {value}")

print_info(name="Alice", age=30, city="NYC")
# name: Alice
# age: 30
# city: NYC

Combining All Parameter Types

python
def example(a, b, *args, key1="default", **kwargs):
    print(f"a={a}, b={b}")
    print(f"args={args}")
    print(f"key1={key1}")
    print(f"kwargs={kwargs}")

example(1, 2, 3, 4, key1="custom", x=10, y=20)
# a=1, b=2
# args=(3, 4)
# key1=custom
# kwargs={'x': 10, 'y': 20}

Order of parameters: regular → *args → keyword-only → **kwargs


Return Values

Single Return

python
def square(n):
    return n ** 2

result = square(5)
print(result)  # 25

Multiple Return Values

python
def divide(a, b):
    quotient = a // b
    remainder = a % b
    return quotient, remainder  # Returns a tuple

q, r = divide(17, 5)
print(f"17 ÷ 5 = {q} remainder {r}")  # 17 ÷ 5 = 3 remainder 2

Early Return

python
def find_first_negative(numbers):
    for num in numbers:
        if num < 0:
            return num
    return None  # No negative found

print(find_first_negative([1, 3, -5, 7]))  # -5
print(find_first_negative([1, 2, 3]))       # None

Note: Functions without a return statement (or with bare return) return None.


Variable Scope

python
# Global variable
name = "Global"

def outer():
    # Enclosing variable
    name = "Outer"

    def inner():
        # Local variable
        name = "Inner"
        print(f"Inner: {name}")

    inner()
    print(f"Outer: {name}")

outer()
print(f"Global: {name}")
# Inner: Inner
# Outer: Outer
# Global: Global

The LEGB Rule

Python looks up variables in this order:

ScopeDescription
LocalInside the current function
EnclosingInside enclosing functions
GlobalModule-level variables
Built-inPython's built-in names (print, len, etc.)

Lambda Functions

Anonymous, single-expression functions:

python
# Regular function
def double(x):
    return x * 2

# Lambda equivalent
double = lambda x: x * 2
print(double(5))  # 10

# Lambda with multiple parameters
add = lambda a, b: a + b
print(add(3, 4))  # 7

# Common use: sorting
students = [("Alice", 90), ("Bob", 85), ("Charlie", 92)]

# Sort by score
students.sort(key=lambda s: s[1])
print(students)
# [('Bob', 85), ('Alice', 90), ('Charlie', 92)]

# Sort by name (descending)
students.sort(key=lambda s: s[0], reverse=True)
print(students)
# [('Charlie', 92), ('Bob', 85), ('Alice', 90)]

# With built-in functions
numbers = [1, -2, 3, -4, 5]
positives = list(filter(lambda x: x > 0, numbers))
print(positives)  # [1, 3, 5]

squared = list(map(lambda x: x ** 2, numbers))
print(squared)  # [1, 4, 9, 16, 25]

Higher-Order Functions

Functions that accept or return other functions:

python
# Function as argument
def apply_operation(func, x, y):
    return func(x, y)

def add(a, b):
    return a + b

def multiply(a, b):
    return a * b

print(apply_operation(add, 3, 4))       # 7
print(apply_operation(multiply, 3, 4))  # 12

# Function returning a function
def multiplier(factor):
    def multiply(n):
        return n * factor
    return multiply

double = multiplier(2)
triple = multiplier(3)

print(double(5))   # 10
print(triple(5))   # 15

Built-in Higher-Order Functions

python
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# map() - apply function to each element
squared = list(map(lambda x: x ** 2, numbers))
print(squared)  # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

# filter() - keep elements where function returns True
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)  # [2, 4, 6, 8, 10]

# reduce() - accumulate a single value
from functools import reduce
total = reduce(lambda a, b: a + b, numbers)
print(total)  # 55

# sorted() with key function
words = ["banana", "apple", "cherry", "date"]
by_length = sorted(words, key=len)
print(by_length)  # ['date', 'apple', 'banana', 'cherry']

Type Hints

Python 3.5+ supports type hints for better documentation and IDE support:

python
def add(a: int, b: int) -> int:
    return a + b

def greet(name: str, times: int = 1) -> str:
    return (f"Hello, {name}! ") * times

def process_items(items: list[str]) -> dict[str, int]:
    return {item: len(item) for item in items}

# Optional type
from typing import Optional

def find_user(user_id: int) -> Optional[dict]:
    """Returns user dict or None if not found."""
    users = {1: {"name": "Alice"}, 2: {"name": "Bob"}}
    return users.get(user_id)

Note: Type hints are not enforced at runtime. They're for documentation and tools like mypy.


Practical Example: Student Grade Calculator

python
"""
Student grade calculator using functions.
"""

def calculate_average(*scores: float) -> float:
    """Calculate the average of given scores."""
    if not scores:
        return 0.0
    return sum(scores) / len(scores)

def determine_grade(average: float) -> str:
    """Determine letter grade from average score."""
    if average >= 90:
        return "A"
    elif average >= 80:
        return "B"
    elif average >= 70:
        return "C"
    elif average >= 60:
        return "D"
    else:
        return "F"

def format_report(name: str, scores: list[float]) -> str:
    """Generate a formatted grade report."""
    avg = calculate_average(*scores)
    grade = determine_grade(avg)

    lines = [
        f"Student: {name}",
        f"Scores:  {', '.join(str(s) for s in scores)}",
        f"Average: {avg:.1f}",
        f"Grade:   {grade}",
    ]
    width = max(len(line) for line in lines) + 4
    border = "=" * width

    report = f"\n{border}\n"
    for line in lines:
        report += f"  {line}\n"
    report += border
    return report

# Usage
students = {
    "Alice": [92, 88, 95, 91],
    "Bob": [78, 82, 75, 80],
    "Charlie": [95, 98, 92, 97],
}

for name, scores in students.items():
    print(format_report(name, scores))

Summary

  • Functions are defined with def function_name(parameters):
  • Support positional, keyword, and default parameters
  • *args collects extra positional arguments as a tuple
  • **kwargs collects extra keyword arguments as a dictionary
  • Functions can return multiple values (as tuples)
  • Lambda functions are anonymous single-expression functions
  • Python follows the LEGB rule for variable scope
  • Higher-order functions accept or return other functions
  • Use type hints for documentation and IDE support
  • Never use mutable objects as default parameter values

Next, we'll learn about Python lists.