Skip to main content

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)
Docstrings

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"))
Mutable Default Arguments

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]
Lambda vs Regular Functions
  • 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)
Avoid Global Variables

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
Recursion Limits

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

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

ConceptDescriptionExample
Function definitionCreate reusable codedef name():
ParametersInput to functiondef func(a, b):
ReturnOutput from functionreturn result
Default argsOptional parametersdef func(x=10):
*argsVariable positional argsdef func(*args):
**kwargsVariable keyword argsdef func(**kwargs):
LambdaAnonymous functionlambda x: x**2
ScopeVariable visibilityLocal, Global, Nonlocal
RecursionFunction calls itselfreturn func(n-1)
Type hintsType annotationsdef func(x: int) -> str:
Key Takeaways
  • Functions should have a single, well-defined purpose
  • Use descriptive names and docstrings
  • Avoid global variables; prefer parameters and returns
  • Use *args and **kwargs for 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
Solutions

Try solving these exercises on your own first. Solutions will be provided in the practice section.