Module 6 - Functions
This module covers functions in Python. You'll learn how to define reusable code blocks, work with parameters and arguments, use *args and **kwargs, and understand scope.
1. Defining Functions
1.1 Basic Function Definition
# Simple function
def greet():
print("Hello, World!")
greet() # Call the function
# Function with parameters
def greet_person(name):
print(f"Hello, {name}!")
greet_person("Alice") # Hello, Alice!
# Function with return value
def add(a, b):
return a + b
result = add(5, 3)
print(result) # 8
1.2 Docstrings
def calculate_area(radius):
"""
Calculate the area of a circle.
Args:
radius (float): The radius of the circle
Returns:
float: The area of the circle
Example:
>>> calculate_area(5)
78.53981633974483
"""
import math
return math.pi * radius ** 2
# Access docstring
print(calculate_area.__doc__)
help(calculate_area)
Always write docstrings for your functions. They serve as documentation and can be accessed using help() or __doc__.
2. Parameters and Arguments
2.1 Positional Arguments
def introduce(name, age, city):
print(f"{name} is {age} years old and lives in {city}")
# Order matters
introduce("Alice", 25, "NYC")
# introduce(25, "Alice", "NYC") # Wrong order!
2.2 Keyword Arguments
# Can specify arguments by name
introduce(name="Bob", age=30, city="LA")
introduce(city="Chicago", name="Charlie", age=35) # Order doesn't matter
# Mix positional and keyword (positional must come first)
introduce("David", age=40, city="Boston")
2.3 Default Parameters
def greet(name, greeting="Hello"):
print(f"{greeting}, {name}!")
greet("Alice") # Hello, Alice!
greet("Bob", "Hi") # Hi, Bob!
greet("Charlie", greeting="Hey") # Hey, Charlie!
# Multiple defaults
def create_profile(name, age=18, country="USA"):
return {"name": name, "age": age, "country": country}
print(create_profile("Alice"))
print(create_profile("Bob", 25))
print(create_profile("Charlie", country="UK"))
Never use mutable objects (lists, dicts) as default arguments! They persist across function calls.
# Bad
def add_item(item, items=[]):
items.append(item)
return items
# Good
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
3. Return Values
3.1 Single Return Value
def square(x):
return x ** 2
result = square(5)
print(result) # 25
# Early return
def divide(a, b):
if b == 0:
return None # or raise an exception
return a / b
3.2 Multiple Return Values
# Return tuple
def get_min_max(numbers):
return min(numbers), max(numbers)
minimum, maximum = get_min_max([1, 5, 3, 9, 2])
print(minimum, maximum) # 1 9
# Return dictionary
def get_stats(numbers):
return {
"min": min(numbers),
"max": max(numbers),
"avg": sum(numbers) / len(numbers)
}
stats = get_stats([1, 2, 3, 4, 5])
print(stats["avg"]) # 3.0
3.3 No Return (None)
def print_message(msg):
print(msg)
# No return statement
result = print_message("Hello")
print(result) # None
# Explicit None
def validate(value):
if value > 0:
return True
return None # Optional, implicit
4. Variable-Length Arguments
4.1 *args (Arbitrary Positional Arguments)
# Accept any number of positional arguments
def sum_all(*args):
total = 0
for num in args:
total += num
return total
print(sum_all(1, 2, 3)) # 6
print(sum_all(1, 2, 3, 4, 5)) # 15
print(sum_all()) # 0
# args is a tuple
def print_args(*args):
print(type(args)) # <class 'tuple'>
print(args)
print_args(1, 2, 3) # (1, 2, 3)
4.2 **kwargs (Arbitrary Keyword Arguments)
# Accept any number of keyword arguments
def print_info(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_info(name="Alice", age=25, city="NYC")
# name: Alice
# age: 25
# city: NYC
# kwargs is a dictionary
def create_user(**kwargs):
print(type(kwargs)) # <class 'dict'>
return kwargs
user = create_user(username="alice", email="alice@example.com")
print(user) # {'username': 'alice', 'email': 'alice@example.com'}
4.3 Combining Parameters
# Order: positional, *args, keyword, **kwargs
def complex_function(required, *args, default="value", **kwargs):
print(f"Required: {required}")
print(f"Args: {args}")
print(f"Default: {default}")
print(f"Kwargs: {kwargs}")
complex_function(
"must_provide",
1, 2, 3,
default="custom",
extra1="value1",
extra2="value2"
)
4.4 Unpacking Arguments
# Unpacking list/tuple
def add(a, b, c):
return a + b + c
numbers = [1, 2, 3]
result = add(*numbers) # Same as add(1, 2, 3)
print(result) # 6
# Unpacking dictionary
def greet(name, age):
print(f"{name} is {age} years old")
person = {"name": "Alice", "age": 25}
greet(**person) # Same as greet(name="Alice", age=25)
5. Lambda Functions
5.1 Basic Lambda
# Regular function
def square(x):
return x ** 2
# Lambda equivalent
square = lambda x: x ** 2
print(square(5)) # 25
# Lambda with multiple parameters
add = lambda a, b: a + b
print(add(3, 4)) # 7
# Lambda without parameters
get_pi = lambda: 3.14159
print(get_pi()) # 3.14159
5.2 Lambda in Built-in Functions
# sorted() with key
words = ["banana", "apple", "cherry", "date"]
sorted_words = sorted(words, key=lambda x: len(x))
print(sorted_words) # ['date', 'apple', 'banana', 'cherry']
# max() with key
students = [
{"name": "Alice", "score": 85},
{"name": "Bob", "score": 92},
{"name": "Charlie", "score": 78}
]
top_student = max(students, key=lambda s: s["score"])
print(top_student) # {'name': 'Bob', 'score': 92}
# map()
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
print(squared) # [1, 4, 9, 16, 25]
# filter()
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [2, 4]
- Use lambdas for simple, one-line operations
- Use regular functions for complex logic or when you need docstrings
- Lambdas are anonymous (no name required)
6. Scope and Namespaces
6.1 Local vs Global Scope
# Global variable
x = 10
def print_x():
print(x) # Access global x
print_x() # 10
# Local variable
def create_local():
y = 20 # Local to function
print(y)
create_local() # 20
# print(y) # NameError: y not defined
6.2 Global Keyword
count = 0
def increment():
global count # Modify global variable
count += 1
increment()
increment()
print(count) # 2
# Without global
def bad_increment():
count += 1 # UnboundLocalError
# Better approach: return value
def good_increment(value):
return value + 1
count = good_increment(count)
Minimize use of global variables. They make code harder to test and maintain. Prefer passing values as parameters and returning results.
6.3 Nonlocal Keyword
# For nested functions
def outer():
x = 10
def inner():
nonlocal x # Refer to outer's x
x += 1
print(f"Inner: {x}")
inner()
print(f"Outer: {x}")
outer()
# Inner: 11
# Outer: 11
6.4 LEGB Rule
# LEGB: Local, Enclosing, Global, Built-in
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # local
inner()
print(x) # enclosing
outer()
print(x) # global
# Built-in
print(len([1, 2, 3])) # Built-in function
7. Recursive Functions
7.1 Basic Recursion
# Factorial
def factorial(n):
if n == 0 or n == 1:
return 1
return n * factorial(n - 1)
print(factorial(5)) # 120
# Fibonacci
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(6)) # 8
7.2 Recursion with Accumulator
# Better factorial with accumulator
def factorial(n, acc=1):
if n == 0:
return acc
return factorial(n - 1, acc * n)
print(factorial(5)) # 120
# Sum of list
def sum_list(lst):
if not lst:
return 0
return lst[0] + sum_list(lst[1:])
print(sum_list([1, 2, 3, 4, 5])) # 15
Python has a recursion limit (default 1000). For large inputs, use iteration or increase the limit with sys.setrecursionlimit().
8. Higher-Order Functions
8.1 Functions as Arguments
def apply_operation(func, value):
return func(value)
def square(x):
return x ** 2
def cube(x):
return x ** 3
print(apply_operation(square, 5)) # 25
print(apply_operation(cube, 5)) # 125
# With lambda
print(apply_operation(lambda x: x * 2, 5)) # 10
8.2 Functions Returning Functions
def multiplier(factor):
def multiply(x):
return x * factor
return multiply
double = multiplier(2)
triple = multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# Practical example: logger
def create_logger(prefix):
def log(message):
print(f"[{prefix}] {message}")
return log
info = create_logger("INFO")
error = create_logger("ERROR")
info("Application started") # [INFO] Application started
error("Something went wrong") # [ERROR] Something went wrong
9. Function Annotations
9.1 Type Hints
# Type hints for parameters and return
def greet(name: str, age: int) -> str:
return f"{name} is {age} years old"
# With default values
def create_user(name: str, age: int = 18) -> dict:
return {"name": name, "age": age}
# Multiple types with Union
from typing import Union
def process(value: Union[int, str]) -> str:
return str(value)
# Optional (can be None)
from typing import Optional
def find_user(user_id: int) -> Optional[dict]:
# May return None if not found
return None
Type hints don't enforce types at runtime. Use tools like mypy for static type checking. They improve code documentation and IDE support.
10. Best Practices
10.1 Function Design Principles
# Single Responsibility
# Bad: Function does too much
def process_user_data(data):
# Validate
# Transform
# Save to database
# Send email
pass
# Good: Separate concerns
def validate_user_data(data):
pass
def transform_user_data(data):
pass
def save_user_data(data):
pass
def notify_user(data):
pass
10.2 Naming Conventions
# Use descriptive names
def calculate_total_price(items, tax_rate): # Good
pass
def calc(i, t): # Bad: Too short
pass
# Use verbs for functions
def get_user(): # Good
pass
def send_email(): # Good
pass
def user(): # Bad: Not a verb
pass
10.3 Keep Functions Small
# Functions should do one thing well
def process_order(order):
if not validate_order(order):
return None
total = calculate_total(order)
apply_discount(order, total)
process_payment(order)
send_confirmation(order)
return order
11. Summary
| Concept | Description | Example |
|---|---|---|
| Function definition | Create reusable code | def name(): |
| Parameters | Input to function | def func(a, b): |
| Return | Output from function | return result |
| Default args | Optional parameters | def func(x=10): |
| *args | Variable positional args | def func(*args): |
| **kwargs | Variable keyword args | def func(**kwargs): |
| Lambda | Anonymous function | lambda x: x**2 |
| Scope | Variable visibility | Local, Global, Nonlocal |
| Recursion | Function calls itself | return func(n-1) |
| Type hints | Type annotations | def func(x: int) -> str: |
- Functions should have a single, well-defined purpose
- Use descriptive names and docstrings
- Avoid global variables; prefer parameters and returns
- Use
*argsand**kwargsfor flexible APIs - Type hints improve code documentation
- Keep functions small and focused
12. What's Next?
In Module 7 - Modules & Packages, you'll learn:
- Importing modules
- Creating your own modules
- Understanding
__name__and__main__ - Package structure
- Virtual environments
13. Practice Exercises
Exercise 1: Temperature Converter
Create functions to convert between Celsius, Fahrenheit, and Kelvin.
def celsius_to_fahrenheit(celsius):
# Your code here
pass
Exercise 2: Validator Functions
Create validator functions for email, phone number, and password.
def is_valid_email(email):
# Your code here
pass
Exercise 3: Calculator with Lambdas
Create a calculator using a dictionary of lambda functions.
operations = {
"add": lambda a, b: a + b,
# Add more operations
}
Exercise 4: Memoization
Implement a memoized Fibonacci function using a cache dictionary.
def fibonacci_memo(n, cache=None):
# Your code here
pass
Exercise 5: Function Decorator Simulation
Create a function that takes another function and adds logging before/after execution.
def add_logging(func):
# Your code here
pass
Try solving these exercises on your own first. Solutions will be provided in the practice section.