Functions

Creating reusable, organized code blocks

Introduction

Functions are one of the most important concepts in programming. They allow you to organize code into reusable blocks, avoid repetition, and make your programs more maintainable. A function is a named block of code that performs a specific task and can be called multiple times throughout your program. Functions help you write cleaner, more modular, and easier-to-debug code.

Defining and Calling Functions

To use a function, you first need to define it, then you can call it whenever you need it.

Basic Function Definition

# Define a function
def greet():
    print("Hello, World!")
    print("Welcome to Python!")

# Call the function
greet()
greet()  # Can call multiple times
Expected Output:
Hello, World!
Welcome to Python!
Hello, World!
Welcome to Python!

Key points: Use the def keyword, function name, parentheses, colon, and an indented function body. Function names follow the same rules as variable names (use snake_case).

Function Structure

def function_name(parameters):
    """
    Docstring describing what the function does.
    This is optional but highly recommended!
    """
    # Function body
    # Code that performs the task
    return value  # Optional

# Example
def calculate_area(length, width):
    """Calculate the area of a rectangle."""
    area = length * width
    return area

Why Use Functions?

✓ Benefits
  • Reusability: Write once, use many times
  • Modularity: Break code into logical pieces
  • Maintainability: Fix bugs in one place
  • Readability: Descriptive names explain purpose
  • Testing: Test functions independently
✗ Without Functions
  • Repeated code blocks
  • Hard to maintain
  • Difficult to debug
  • Long, hard-to-read code
  • Changes require multiple edits

Parameters and Arguments

Functions can accept input values called parameters (in the definition) and arguments (when calling the function).

Function with One Parameter

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

greet("Alice")
greet("Bob")
Expected Output:
Hello, Alice!
Hello, Bob!

Function with Multiple Parameters

def add_numbers(a, b):
    result = a + b
    print(f"{a} + {b} = {result}")

add_numbers(5, 3)
add_numbers(10, 20)
Expected Output:
5 + 3 = 8
10 + 20 = 30

Positional Arguments

Arguments are matched to parameters by position:

def introduce(name, age, city):
    print(f"Hi, I'm {name}, {age} years old, from {city}")

# Positional arguments (order matters!)
introduce("Alice", 30, "New York")

# Wrong order = wrong result!
introduce("New York", "Alice", 30)
Expected Output:
Hi, I'm Alice, 30 years old, from New York
Hi, I'm New York, Alice years old, from 30

Keyword Arguments

You can specify arguments by name, which makes code more readable:

def introduce(name, age, city):
    print(f"Hi, I'm {name}, {age} years old, from {city}")

# Keyword arguments (order doesn't matter)
introduce(name="Alice", age=30, city="New York")
introduce(city="Boston", name="Bob", age=25)

# Mix positional and keyword (positional first)
introduce("Alice", age=30, city="New York")
# introduce(age=30, "Alice", city="New York")  # SyntaxError: positional after keyword
Expected Output:
Hi, I'm Alice, 30 years old, from New York
Hi, I'm Bob, 25 years old, from Boston
Hi, I'm Alice, 30 years old, from New York

Default Parameters

You can provide default values for parameters. If the caller omits them, the default is used:

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

greet("Alice")                        # uses default greeting
greet("Bob", "Hi")                    # positional override
greet("Charlie", greeting="Hey")      # keyword override
Expected Output:
Hello, Alice!
Hi, Bob!
Hey, Charlie!

Multiple Default Parameters

Functions can have several default parameters. You can override any subset of them, in any order, using keyword arguments:

def create_profile(name, age=0, city="Unknown", active=True):
    print(f"Name: {name}, Age: {age}, City: {city}, Active: {active}")

create_profile("Alice")                          # all defaults
create_profile("Bob", 25)                        # override age
create_profile("Charlie", city="Boston")         # skip age, override city
create_profile("Diana", 30, "New York", False)   # override everything
Expected Output:
Name: Alice, Age: 0, City: Unknown, Active: True
Name: Bob, Age: 25, City: Unknown, Active: True
Name: Charlie, Age: 0, City: Boston, Active: True
Name: Diana, Age: 30, City: New York, Active: False

Important: Parameters with default values must come after parameters without defaults.

Variable-Length Arguments

Use *args for multiple positional arguments and **kwargs for multiple keyword arguments:

# *args - variable number of positional arguments
def sum_numbers(*args):
    total = 0
    for num in args:
        total += num
    return total

print(sum_numbers(1, 2, 3))
print(sum_numbers(1, 2, 3, 4, 5))

# **kwargs - variable number of keyword arguments
def print_info(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_info(name="Alice", age=30, city="NYC")
Expected Output:
6
15
name: Alice
age: 30
city: NYC

Return Values

Functions can return values using the return statement. This allows you to use the function's result in other parts of your code.

Basic Return

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

print(add(5, 3))

# Can use directly
print(add(10, 20))

# Can use in expressions
total = add(5, 3) + add(2, 4)
print(total)
Expected Output:
8
30
14

Multiple Return Values

Functions can return multiple values as a tuple:

def calculate_stats(numbers):
    total = sum(numbers)
    count = len(numbers)
    average = total / count if count > 0 else 0
    maximum = max(numbers) if numbers else 0
    minimum = min(numbers) if numbers else 0
    return total, average, maximum, minimum

scores = [85, 90, 88, 92, 87]
total, avg, max_score, min_score = calculate_stats(scores)

print(f"Total: {total}, Average: {avg:.1f}")
print(f"Max: {max_score}, Min: {min_score}")

# Or receive as tuple
result = calculate_stats(scores)
print(result)
Expected Output:
Total: 442, Average: 88.4
Max: 92, Min: 85
(442, 88.4, 92, 85)

Early Return

You can (and often should) return early from a function. This is called "early return" and is recommended because it helps make your code simpler, clearer, and easier to read. By returning as soon as you've handled an error, met a condition, or finished your work, you avoid unnecessary nesting and complex logic.

def check_age(age):
    if age < 0:
        return "Invalid age"
    if age < 18:
        return "Minor"
    if age < 65:
        return "Adult"
    return "Senior"

print(check_age(25))
print(check_age(70))
print(check_age(-5))

# Function without return returns None
def no_return():
    print("This function doesn't return anything")

result = no_return()
print(result)
Expected Output:
Adult
Senior
Invalid age
This function doesn't return anything
None

Scope and Local/Global Variables

Scope determines where a variable can be accessed. Understanding scope is crucial for writing correct functions.

Local Variables

Variables defined inside a function are local to that function:

def my_function():
    local_var = "I'm local"
    print(local_var)

my_function()
# print(local_var)   # NameError: local_var is not defined outside function

# Each function call creates new local variables
def counter():
    count = 0
    count += 1
    return count

print(counter())
print(counter())  # Still 1 — a new local variable is created each call
Expected Output:
I'm local
1
1

Global Variables

Variables defined outside functions are global and can be read inside functions:

# Global variable
global_var = "I'm global"

def read_global():
    print(global_var)  # Can read global variable

read_global()

# Without 'global' keyword, assignment creates a LOCAL variable
def try_modify():
    global_var = "Trying to change"  # Creates a LOCAL variable, not modifying global!
    print(global_var)

try_modify()
print(global_var)  # Global is unchanged
Expected Output:
I'm global
Trying to change
I'm global

The global Keyword

To modify a global variable inside a function, use the global keyword:

count = 0  # Global variable

def increment():
    global count  # Declare we're modifying the global variable
    count += 1
    print(f"Count is now {count}")

increment()
increment()
print(count)

# Best practice: avoid global variables when possible
# Pass values as parameters and return results instead
Expected Output:
Count is now 1
Count is now 2
2

Best Practice: Avoid using global variables when possible. It's better to pass values as parameters and return results. Global variables make code harder to understand and debug.

Scope Example

x = "global"  # Global variable

def outer():
    x = "outer"  # Local to outer()

    def inner():
        x = "inner"  # Local to inner()
        print(f"inner: x = {x}")

    inner()
    print(f"outer: x = {x}")

outer()
print(f"global: x = {x}")
Expected Output:
inner: x = inner
outer: x = outer
global: x = global

Lambda Functions

Lambda functions are small, anonymous functions defined with the lambda keyword. They're useful for short, simple operations.

Basic Lambda Syntax

# Regular function
def add(a, b):
    return a + b

# Lambda function (equivalent)
add_lambda = lambda a, b: a + b

print(add(5, 3))
print(add_lambda(5, 3))

# Syntax: lambda parameters: expression
# No return keyword needed — result is returned implicitly
Expected Output:
8
8

Common Use Cases

Lambda functions are often used with built-in functions like map(), filter(), and sorted():

numbers = [1, 2, 3, 4, 5]

# map() - apply function to each element
squares = list(map(lambda x: x ** 2, numbers))
print(squares)

# filter() - keep elements that meet condition
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)

# sorted() - custom sorting key
students = [("Alice", 25), ("Bob", 20), ("Charlie", 30)]
sorted_by_age = sorted(students, key=lambda student: student[1])
print(sorted_by_age)
Expected Output:
[1, 4, 9, 16, 25]
[2, 4]
[('Bob', 20), ('Alice', 25), ('Charlie', 30)]

Lambda vs Regular Functions

Use Lambda When:
  • Short, simple operations
  • One-line expressions
  • Used as arguments to other functions
  • Temporary, throwaway functions
Use Regular Function When:
  • Complex logic
  • Multiple statements
  • Needs documentation
  • Reused multiple times
  • Needs to be testable

More Lambda Examples

# Simple operation
double = lambda x: x * 2
print(double(5))

# Multiple parameters
multiply = lambda x, y: x * y
print(multiply(3, 4))

# Conditional expression in lambda
max_value = lambda a, b: a if a > b else b
print(max_value(5, 3))

# Default parameter in lambda
power = lambda x, n=2: x ** n
print(power(5))
print(power(5, 3))
Expected Output:
10
12
5
25
125

Practical Examples

Example: Calculator Functions

def calculate(operation, a, b):
    """Perform a mathematical operation on two numbers."""
    if operation == "add":
        return a + b
    elif operation == "subtract":
        return a - b
    elif operation == "multiply":
        return a * b
    elif operation == "divide":
        if b != 0:
            return a / b
        else:
            return "Error: Division by zero"
    else:
        return "Error: Unknown operation"

print(calculate("add", 10, 5))
print(calculate("multiply", 4, 7))
print(calculate("divide", 20, 4))
print(calculate("divide", 10, 0))
Expected Output:
15
28
5.0
Error: Division by zero

Example: Data Processing Functions

def process_students(students, min_grade=80):
    """
    Filter and process student data.

    Args:
        students: List of dictionaries with 'name' and 'grade' keys
        min_grade: Minimum grade threshold (default: 80)

    Returns:
        List of students who meet the grade threshold
    """
    passed = []
    for student in students:
        if student["grade"] >= min_grade:
            passed.append({
                "name": student["name"],
                "grade": student["grade"],
                "status": "Pass"
            })
    return passed

students = [
    {"name": "Alice",   "grade": 85},
    {"name": "Bob",     "grade": 78},
    {"name": "Charlie", "grade": 92},
    {"name": "Diana",   "grade": 88}
]

top_students = process_students(students, min_grade=85)
for student in top_students:
    print(f"{student['name']}: {student['grade']} ({student['status']})")
Expected Output:
Alice: 85 (Pass)
Charlie: 92 (Pass)
Diana: 88 (Pass)

Intermediate: * and / in Function Parameters

In Python 3, you can use * and / in your function definitions to control how arguments are passed:

  • * marks the end of positional arguments. Parameters after * must be specified by keyword.
  • / marks the end of parameters that must be passed positionally. Parameters before / cannot be specified by keyword.(Available in Python 3.8+)
def greet(name, /, greeting="Hello"):
    # 'name' must be positional; 'greeting' can be positional or keyword
    print(f"{greeting}, {name}!")

greet("Alice")                  # OK
greet("Alice", greeting="Hi")  # OK
# greet(name="Alice")          # TypeError — 'name' is positional-only

def add(a, b, *, show=False):
    # 'show' MUST be passed by keyword
    res = a + b
    if show:
        print(f"Result: {res}")
    return res

add(3, 4)              # OK, show defaults to False
add(3, 4, show=True)   # OK
# add(3, 4, True)      # TypeError — 'show' must be a keyword argument
Expected Output:
Hello, Alice!
Hi, Alice!
Result: 7

Tip: Using * and / makes your APIs clearer and helps prevent bugs from argument misplacement.

Key Takeaways

Function Basics

  • Use def to define functions
  • Functions make code reusable and modular
  • Call functions by name with parentheses
  • Add docstrings to document functions

Parameters & Arguments

  • Positional and keyword arguments
  • Default parameter values
  • *args for variable arguments
  • **kwargs for keyword arguments

Return & Scope

  • return sends values back
  • Functions can return multiple values
  • Local vs global variables
  • Avoid global variables when possible

Lambda Functions

  • Anonymous, one-line functions
  • Useful with map(), filter()
  • Keep lambdas simple
  • Use regular functions for complex logic
What's Next?

Great job! Functions are fundamental to writing clean, maintainable code. You now have the tools to organize your programs effectively. In future lessons, we'll explore:

  • Error handling - Handle exceptions gracefully with try/except blocks
  • Working with files - Read from and write to files safely
  • Modules and imports - Organize code across multiple files
  • Object-oriented programming - Build programs with classes and objects