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 timesExpected 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 areaWhy 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 keywordExpected 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 overrideExpected 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 everythingExpected 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 callExpected 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 unchangedExpected 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 insteadExpected 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 implicitlyExpected 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 argumentExpected 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
defto 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
*argsfor variable arguments**kwargsfor keyword arguments
Return & Scope
returnsends 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