Python Functions Complete Guide
Learn all Python function concepts - definition, parameters, return values, lambda functions, decorators, scope, and recursion with practical examples.
Modular
Reusable code
Parameters
Flexible inputs
Lambda
Anonymous functions
Decorators
Function wrappers
What are Python Functions?
Functions in Python are reusable blocks of code that perform specific tasks. They help in organizing code, reducing repetition, and making programs more modular and maintainable.
Key Concept
Functions follow the DRY (Don't Repeat Yourself) principle. They take inputs (parameters), perform operations, and return outputs. Functions are defined using the def keyword.
# Function definition
def greet():
"""Simple function that prints a greeting"""
print("Hello, World!")
# Function call
greet() # Output: Hello, World!
# Function with parameters
def greet_person(name):
"""Greet a specific person"""
print(f"Hello, {name}!")
greet_person("Alice") # Output: Hello, Alice!
greet_person("Bob") # Output: Hello, Bob!
# Function with return value
def add_numbers(a, b):
"""Add two numbers and return the result"""
return a + b
result = add_numbers(5, 3)
print(f"5 + 3 = {result}") # Output: 5 + 3 = 8
# Multiple return values (as tuple)
def get_min_max(numbers):
"""Return minimum and maximum from a list"""
return min(numbers), max(numbers)
nums = [5, 2, 8, 1, 9]
minimum, maximum = get_min_max(nums)
print(f"Min: {minimum}, Max: {maximum}") # Output: Min: 1, Max: 9
# Function with default parameters
def greet_with_time(name, time_of_day="morning"):
"""Greet with optional time of day"""
print(f"Good {time_of_day}, {name}!")
greet_with_time("Alice") # Output: Good morning, Alice!
greet_with_time("Bob", "afternoon") # Output: Good afternoon, Bob!
# Function with docstring
def calculate_area(length, width):
"""
Calculate the area of a rectangle.
Parameters:
length (float): Length of the rectangle
width (float): Width of the rectangle
Returns:
float: Area of the rectangle
"""
return length * width
area = calculate_area(10, 5)
print(f"Area: {area}") # Output: Area: 50
# Accessing docstring
print(calculate_area.__doc__)
Python Function Types and Categories
Python supports various types of functions with different characteristics and use cases. Understanding these helps in writing better, more efficient code.
Complete Function Types Reference Table
| Category | Function Type | Description | Syntax Example | Use Case |
|---|---|---|---|---|
| Basic Functions | Simple Function | Basic function without parameters | def func(): pass |
Simple reusable tasks |
| Basic Functions | Parameterized Function | Function with input parameters | def func(a, b): pass |
Tasks requiring input |
| Basic Functions | Return Function | Function that returns a value | return value |
Calculations, data processing |
| Parameter Types | Positional Arguments | Arguments passed in order | func(1, 2) |
Required parameters |
| Parameter Types | Keyword Arguments | Arguments passed by name | func(a=1, b=2) |
Clarity, optional params |
| Parameter Types | Default Parameters | Parameters with default values | def func(a=1): pass |
Optional parameters |
| Parameter Types | *args (Variable-length) | Accepts any number of positional args | def func(*args): pass |
Flexible number of inputs |
| Parameter Types | **kwargs (Keyword args) | Accepts any number of keyword args | def func(**kwargs): pass |
Flexible keyword inputs |
| Advanced Functions | Lambda Function | Anonymous inline function | lambda x: x*2 |
Simple one-liners |
| Advanced Functions | Recursive Function | Function that calls itself | def func(): func() |
Tree traversal, factorial |
| Advanced Functions | Generator Function | Function with yield statement | yield value |
Memory-efficient iteration |
| Advanced Functions | Higher-order Function | Function that takes/returns functions | map(func, iterable) |
Functional programming |
| Scope & Visibility | Local Function | Function defined inside another | def outer(): def inner(): |
Encapsulation, closures |
| Scope & Visibility | Closure | Function that remembers enclosing scope | def outer(): def inner(): |
Data encapsulation |
| Scope & Visibility | Global Function | Function defined at module level | def func(): (module level) |
Module-wide utilities |
| Decorators | Function Decorator | Function that modifies another function | @decorator |
Cross-cutting concerns |
| Decorators | Class Decorator | Decorator implemented as class | class Decorator: |
Stateful decorators |
| Decorators | Parameterized Decorator | Decorator that accepts arguments | @decorator(args) |
Configurable decoration |
| Special Functions | Built-in Functions | Python's pre-defined functions | len(), print(), type() |
Common operations |
| Special Functions | Method | Function bound to an object | obj.method() |
Object-oriented programming |
- DRY (Don't Repeat Yourself): Use functions to avoid code duplication
- Single Responsibility: Each function should do one thing well
- Pure Functions: Functions without side effects are easier to test and debug
- Function Composition: Combine simple functions to build complex behavior
- Immutability: Prefer returning new values over modifying inputs
Function Parameters in Detail
Python offers flexible parameter passing mechanisms. Understanding different parameter types helps write versatile and robust functions.
# 1. Positional Arguments (Required)
def describe_pet(animal_type, pet_name):
"""Display information about a pet"""
print(f"I have a {animal_type} named {pet_name}.")
describe_pet("dog", "Rex") # I have a dog named Rex.
describe_pet("cat", "Whiskers") # I have a cat named Whiskers.
# 2. Keyword Arguments
def describe_person(name, age, city):
"""Describe a person using keyword arguments"""
print(f"{name} is {age} years old and lives in {city}.")
# All keyword arguments
describe_person(name="Alice", age=30, city="NYC")
# Mix of positional and keyword (positional must come first)
describe_person("Bob", city="London", age=25)
# 3. Default Parameters
def greet(name, greeting="Hello", punctuation="!"):
"""Greet with customizable greeting and punctuation"""
print(f"{greeting}, {name}{punctuation}")
greet("Alice") # Hello, Alice!
greet("Bob", "Hi") # Hi, Bob!
greet("Charlie", "Hey", ".") # Hey, Charlie.
# Important: Mutable default arguments
def add_item(item, items=[]): # WARNING: Default list is shared!
items.append(item)
return items
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['apple', 'banana'] (UNEXPECTED!)
# Correct way with None
def add_item_safe(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(add_item_safe("apple")) # ['apple']
print(add_item_safe("banana")) # ['banana'] (CORRECT!)
# 4. *args - Variable-length positional arguments
def sum_all(*args):
"""Sum all provided numbers"""
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 in function calls
def print_three(a, b, c):
print(f"a={a}, b={b}, c={c}")
numbers = [1, 2, 3]
print_three(*numbers) # a=1, b=2, c=3 (unpacking)
# 5. **kwargs - Variable-length keyword arguments
def print_info(**kwargs):
"""Print all key-value pairs"""
for key, value in kwargs.items():
print(f"{key}: {value}")
print_info(name="Alice", age=30, city="NYC")
# Output:
# name: Alice
# age: 30
# city: NYC
# **kwargs in function calls
def greet_person(name, age):
print(f"{name} is {age} years old")
person_info = {"name": "Bob", "age": 25}
greet_person(**person_info) # Bob is 25 years old
# 6. Combined parameter types
def complex_function(a, b, *args, option=True, **kwargs):
"""Function with all parameter types"""
print(f"a={a}, b={b}")
print(f"args={args}")
print(f"option={option}")
print(f"kwargs={kwargs}")
complex_function(1, 2, 3, 4, 5, option=False, x=10, y=20)
# Output:
# a=1, b=2
# args=(3, 4, 5)
# option=False
# kwargs={'x': 10, 'y': 20}
# 7. Positional-only and keyword-only arguments (Python 3.8+)
def advanced_func(a, b, /, c, d, *, e, f):
"""
a, b: positional-only (before /)
c, d: positional or keyword
e, f: keyword-only (after *)
"""
return a + b + c + d + e + f
# Valid calls
advanced_func(1, 2, 3, d=4, e=5, f=6)
advanced_func(1, 2, c=3, d=4, e=5, f=6)
# Invalid calls
# advanced_func(a=1, b=2, c=3, d=4, e=5, f=6) # a,b are positional-only
# advanced_func(1, 2, 3, 4, 5, 6) # e,f must be keyword
# 8. Type hints (Python 3.5+)
def calculate_total(price: float, quantity: int, tax_rate: float = 0.08) -> float:
"""Calculate total price with tax"""
subtotal = price * quantity
tax = subtotal * tax_rate
return subtotal + tax
total = calculate_total(10.5, 3)
print(f"Total: ${total:.2f}") # Total: $34.02
Lambda Functions (Anonymous Functions)
Lambda functions are small, anonymous functions defined with the lambda keyword. They are useful for short, simple operations.
# Basic lambda syntax
# lambda arguments: expression
# 1. Simple lambda functions
square = lambda x: x ** 2
print(square(5)) # 25
add = lambda a, b: a + b
print(add(10, 20)) # 30
# 2. Lambda with conditional expression
is_even = lambda x: True if x % 2 == 0 else False
print(is_even(4)) # True
print(is_even(5)) # False
# Shorter version
is_even = lambda x: x % 2 == 0
# 3. Lambda with multiple conditions
grade = lambda score: 'A' if score >= 90 else 'B' if score >= 80 else 'C' if score >= 70 else 'D' if score >= 60 else 'F'
print(grade(85)) # B
print(grade(95)) # A
# 4. Lambda in higher-order functions
numbers = [1, 2, 3, 4, 5]
# map() with lambda
squared = list(map(lambda x: x ** 2, numbers))
print(f"Squared: {squared}") # [1, 4, 9, 16, 25]
# filter() with lambda
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(f"Evens: {evens}") # [2, 4]
# sorted() with lambda
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 72},
{"name": "Charlie", "grade": 90}
]
# Sort by grade
sorted_students = sorted(students, key=lambda x: x["grade"], reverse=True)
print(f"Sorted by grade: {sorted_students}")
# Sort by name length
sorted_by_name_len = sorted(students, key=lambda x: len(x["name"]))
print(f"Sorted by name length: {sorted_by_name_len}")
# 5. Lambda with default arguments
multiply = lambda x, y=2: x * y
print(multiply(5)) # 10 (uses default y=2)
print(multiply(5, 3)) # 15
# 6. Lambda returning multiple values (as tuple)
stats = lambda lst: (min(lst), max(lst), sum(lst)/len(lst))
numbers = [10, 20, 30, 40]
minimum, maximum, average = stats(numbers)
print(f"Min: {minimum}, Max: {maximum}, Avg: {average}")
# 7. Lambda in list comprehensions
operations = [
lambda x: x + 1,
lambda x: x * 2,
lambda x: x ** 2
]
result = [op(5) for op in operations]
print(f"Operations on 5: {result}") # [6, 10, 25]
# 8. Lambda with *args and **kwargs
sum_all = lambda *args: sum(args)
print(sum_all(1, 2, 3, 4, 5)) # 15
print_info = lambda **kwargs: {k: v for k, v in kwargs.items()}
info = print_info(name="Alice", age=30)
print(f"Info: {info}") # {'name': 'Alice', 'age': 30}
# 9. Immediately invoked lambda (IIFE - Immediately Invoked Function Expression)
result = (lambda x, y: x + y)(10, 20)
print(f"IIFE result: {result}") # 30
# 10. Lambda for simple data transformation
data = ["apple", "banana", "cherry"]
uppercase = list(map(lambda s: s.upper(), data))
print(f"Uppercase: {uppercase}") # ['APPLE', 'BANANA', 'CHERRY']
# 11. Lambda in dictionary sorting
word_counts = {"apple": 5, "banana": 2, "cherry": 8, "date": 3}
# Sort by value (count)
sorted_by_count = dict(sorted(word_counts.items(), key=lambda item: item[1]))
print(f"Sorted by count: {sorted_by_count}")
# Sort by key length
sorted_by_key_len = dict(sorted(word_counts.items(), key=lambda item: len(item[0])))
print(f"Sorted by key length: {sorted_by_key_len}")
# 12. When NOT to use lambda
# Don't use lambda for complex logic - use regular functions instead
# Bad example (too complex for lambda)
# complex_logic = lambda x: "High" if x > 100 else ("Medium" if x > 50 else ("Low" if x > 10 else "Very Low"))
# Good example - use regular function
def categorize_value(x):
if x > 100:
return "High"
elif x > 50:
return "Medium"
elif x > 10:
return "Low"
else:
return "Very Low"
print(categorize_value(75)) # Medium
# 13. Lambda with functools.reduce()
from functools import reduce
numbers = [1, 2, 3, 4, 5]
product = reduce(lambda x, y: x * y, numbers)
print(f"Product: {product}") # 120
# Find maximum
maximum = reduce(lambda x, y: x if x > y else y, numbers)
print(f"Maximum: {maximum}") # 5
Function Decorators
Decorators are functions that modify the behavior of other functions. They provide a way to add functionality to existing code without modifying it directly.
# 1. Basic decorator
def simple_decorator(func):
"""A simple decorator that adds functionality before and after"""
def wrapper():
print("Before function execution")
func()
print("After function execution")
return wrapper
@simple_decorator
def say_hello():
print("Hello!")
say_hello()
# Output:
# Before function execution
# Hello!
# After function execution
# 2. Decorator for functions with parameters
def decorator_with_params(func):
"""Decorator that handles functions with parameters"""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@decorator_with_params
def add(a, b):
return a + b
result = add(5, 3)
print(f"Result: {result}")
# Output:
# Calling add with args=(5, 3), kwargs={}
# add returned 8
# Result: 8
# 3. Decorator that returns a value
def timing_decorator(func):
"""Decorator that measures execution time"""
import time
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
return result
return wrapper
@timing_decorator
def slow_function():
"""Simulate a slow function"""
import time
time.sleep(1)
return "Done"
slow_function() # Output: slow_function took 1.0001 seconds
# 4. Decorator with arguments (decorator factory)
def repeat(n_times):
"""Decorator factory that creates a decorator to repeat function n times"""
def decorator(func):
def wrapper(*args, **kwargs):
for i in range(n_times):
print(f"Execution {i+1}/{n_times}")
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(n_times=3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Output:
# Execution 1/3
# Hello, Alice!
# Execution 2/3
# Hello, Alice!
# Execution 3/3
# Hello, Alice!
# 5. Multiple decorators (applied from bottom to top)
def decorator1(func):
def wrapper():
print("Decorator 1: Before")
func()
print("Decorator 1: After")
return wrapper
def decorator2(func):
def wrapper():
print("Decorator 2: Before")
func()
print("Decorator 2: After")
return wrapper
@decorator1
@decorator2
def my_function():
print("Original function")
my_function()
# Output:
# Decorator 1: Before
# Decorator 2: Before
# Original function
# Decorator 2: After
# Decorator 1: After
# 6. Class-based decorator
class CountCalls:
"""Decorator class that counts function calls"""
def __init__(self, func):
self.func = func
self.call_count = 0
def __call__(self, *args, **kwargs):
self.call_count += 1
print(f"Call {self.call_count} of {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hi():
print("Hi!")
say_hi() # Call 1 of say_hi
say_hi() # Call 2 of say_hi
say_hi() # Call 3 of say_hi
# 7. Decorator that preserves function metadata
from functools import wraps
def preserve_metadata(func):
"""Decorator that preserves the original function's metadata"""
@wraps(func)
def wrapper(*args, **kwargs):
"""Wrapper function docstring"""
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@preserve_metadata
def calculate(x, y):
"""Calculate the sum of x and y"""
return x + y
print(f"Function name: {calculate.__name__}") # calculate
print(f"Function docstring: {calculate.__doc__}") # Calculate the sum of x and y
print(f"Result: {calculate(5, 3)}") # 8
# 8. Practical decorator examples
# Logging decorator
def log_execution(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[LOG] Executing {func.__name__} with args={args}, kwargs={kwargs}")
try:
result = func(*args, **kwargs)
print(f"[LOG] {func.__name__} returned {result}")
return result
except Exception as e:
print(f"[LOG] {func.__name__} raised {type(e).__name__}: {e}")
raise
return wrapper
@log_execution
def divide(a, b):
return a / b
divide(10, 2) # Logs execution and result
# divide(10, 0) # Logs exception
# Cache decorator (memoization)
def cache_results(func):
"""Cache function results to avoid recomputation"""
cache = {}
@wraps(func)
def wrapper(*args, **kwargs):
# Create a key from args and kwargs
key = (args, frozenset(kwargs.items()))
if key not in cache:
cache[key] = func(*args, **kwargs)
print(f"Calculated result for {args}")
else:
print(f"Using cached result for {args}")
return cache[key]
return wrapper
@cache_results
def fibonacci(n):
"""Calculate nth Fibonacci number (inefficient without cache)"""
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(f"fibonacci(10) = {fibonacci(10)}")
# Without cache: O(2^n) complexity
# With cache: O(n) complexity
# Authentication decorator
def require_auth(func):
"""Decorator that requires authentication"""
@wraps(func)
def wrapper(*args, **kwargs):
# In real application, check authentication here
is_authenticated = True # Simulated
if not is_authenticated:
raise PermissionError("Authentication required")
return func(*args, **kwargs)
return wrapper
@require_auth
def view_profile(user_id):
return f"Viewing profile of user {user_id}"
print(view_profile(123)) # Works if authenticated
Function Scope and Closure
Understanding variable scope is crucial in Python. Functions create their own local scope, and closures allow functions to remember their enclosing scope.
# 1. Variable Scope Levels
# LEGB Rule: Local -> Enclosing -> Global -> Built-in
# Global scope
global_var = "I'm global"
def outer_function():
# Enclosing scope
enclosing_var = "I'm in enclosing scope"
def inner_function():
# Local scope
local_var = "I'm local"
print(local_var) # Local
print(enclosing_var) # Enclosing
print(global_var) # Global
print(len) # Built-in (len function)
inner_function()
# print(local_var) # ERROR: local_var not defined in this scope
outer_function()
# 2. Modifying global variables
counter = 0
def increment():
global counter # Declare we're using the global variable
counter += 1
increment()
increment()
print(f"Counter: {counter}") # 2
# 3. Nonlocal for nested functions
def outer():
count = 0
def inner():
nonlocal count # Refer to count in enclosing scope
count += 1
return count
return inner
counter_func = outer()
print(counter_func()) # 1
print(counter_func()) # 2
print(counter_func()) # 3
# 4. Closures - Functions that remember their environment
def multiplier_factory(factor):
"""Create a multiplier function that remembers the factor"""
def multiplier(number):
return number * factor
return multiplier
double = multiplier_factory(2)
triple = multiplier_factory(3)
print(double(5)) # 10
print(triple(5)) # 15
# The closure remembers factor=2 even after multiplier_factory has returned
print(f"Double closure variables: {double.__closure__}")
print(f"Triple closure variables: {triple.__closure__}")
# 5. Practical closure example - Counter with different increments
def create_counter(initial=0, step=1):
"""Create a counter function with configurable initial value and step"""
count = initial
def counter():
nonlocal count
current = count
count += step
return current
return counter
# Create different counters
counter1 = create_counter() # Starts at 0, increments by 1
counter2 = create_counter(10, 2) # Starts at 10, increments by 2
counter3 = create_counter(100, -5) # Starts at 100, decrements by 5
print("Counter 1:", counter1(), counter1(), counter1()) # 0, 1, 2
print("Counter 2:", counter2(), counter2(), counter2()) # 10, 12, 14
print("Counter 3:", counter3(), counter3(), counter3()) # 100, 95, 90
# 6. Closure with mutable state
def create_accumulator():
"""Create an accumulator that remembers all values"""
values = []
def accumulator(new_value):
values.append(new_value)
return sum(values), values.copy()
return accumulator
acc = create_accumulator()
print(acc(10)) # (10, [10])
print(acc(20)) # (30, [10, 20])
print(acc(30)) # (60, [10, 20, 30])
# 7. Function attributes (alternative to closures for simple cases)
def counter_with_attr():
"""Counter using function attributes instead of closure"""
if not hasattr(counter_with_attr, "count"):
counter_with_attr.count = 0
counter_with_attr.count += 1
return counter_with_attr.count
print(counter_with_attr()) # 1
print(counter_with_attr()) # 2
print(counter_with_attr()) # 3
# 8. Scope resolution in practice
x = "global"
def test_scope():
x = "enclosing"
def inner():
x = "local"
print(f"Inner: x = {x}")
inner()
print(f"Test_scope: x = {x}")
test_scope()
print(f"Global: x = {x}")
# 9. Using globals() and locals() functions
def scope_demo():
local_var = "I'm local"
print("Local variables:", locals())
print("Global variables (keys):", list(globals().keys())[:5])
scope_demo()
# 10. Common scope pitfalls
# Pitfall 1: Unintended global variable creation
def create_global():
global new_global # Without this line, Python creates a local variable
new_global = "Oops, I'm global"
create_global()
print(new_global) # Works, but might be unintended
# Pitfall 2: Late binding in closures
def create_multipliers():
return [lambda x: i * x for i in range(5)]
multipliers = create_multipliers()
print([m(2) for m in multipliers]) # [8, 8, 8, 8, 8] (NOT [0, 2, 4, 6, 8])
# Why? The closure captures i, not its value at creation time
# Solution: Use default arguments
def create_multipliers_fixed():
return [lambda x, i=i: i * x for i in range(5)]
multipliers = create_multipliers_fixed()
print([m(2) for m in multipliers]) # [0, 2, 4, 6, 8] (CORRECT!)
Recursive Functions
Recursion occurs when a function calls itself. It's a powerful technique for solving problems that can be broken down into smaller, similar subproblems.
# 1. Factorial (classic recursion example)
def factorial(n):
"""Calculate factorial recursively"""
# Base case
if n == 0 or n == 1:
return 1
# Recursive case
return n * factorial(n - 1)
print(f"5! = {factorial(5)}") # 120
print(f"0! = {factorial(0)}") # 1
print(f"7! = {factorial(7)}") # 5040
# 2. Fibonacci sequence
def fibonacci(n):
"""Calculate nth Fibonacci number recursively"""
# Base cases
if n == 0:
return 0
elif n == 1:
return 1
# Recursive case
return fibonacci(n - 1) + fibonacci(n - 2)
print(f"Fibonacci(0) = {fibonacci(0)}") # 0
print(f"Fibonacci(1) = {fibonacci(1)}") # 1
print(f"Fibonacci(6) = {fibonacci(6)}") # 8
# 3. Recursive list sum
def recursive_sum(numbers):
"""Sum a list recursively"""
# Base case: empty list
if not numbers:
return 0
# Recursive case: first element + sum of rest
return numbers[0] + recursive_sum(numbers[1:])
numbers = [1, 2, 3, 4, 5]
print(f"Sum of {numbers} = {recursive_sum(numbers)}") # 15
# 4. Recursive list flattening
def flatten_list(nested_list):
"""Flatten a nested list recursively"""
result = []
for item in nested_list:
if isinstance(item, list):
# Recursive call for nested list
result.extend(flatten_list(item))
else:
result.append(item)
return result
nested = [1, [2, 3], [4, [5, 6]], 7]
print(f"Flattened: {flatten_list(nested)}") # [1, 2, 3, 4, 5, 6, 7]
# 5. Recursive binary search
def binary_search(arr, target, low=0, high=None):
"""Binary search using recursion"""
if high is None:
high = len(arr) - 1
# Base case: element not found
if low > high:
return -1
mid = (low + high) // 2
# Base case: element found
if arr[mid] == target:
return mid
# Recursive cases
elif arr[mid] > target:
return binary_search(arr, target, low, mid - 1)
else:
return binary_search(arr, target, mid + 1, high)
sorted_numbers = [1, 3, 5, 7, 9, 11, 13, 15]
print(f"Index of 7: {binary_search(sorted_numbers, 7)}") # 3
print(f"Index of 12: {binary_search(sorted_numbers, 12)}") # -1 (not found)
# 6. Recursive directory traversal
import os
def list_files(path, indent=0):
"""List all files in directory recursively"""
try:
items = os.listdir(path)
except PermissionError:
return
for item in items:
full_path = os.path.join(path, item)
if os.path.isfile(full_path):
print(" " * indent + f"📄 {item}")
elif os.path.isdir(full_path):
print(" " * indent + f"📁 {item}/")
list_files(full_path, indent + 2)
# Uncomment to run (adjust path as needed)
# list_files(".")
# 7. Recursive palindrome checker
def is_palindrome(s):
"""Check if string is palindrome recursively"""
# Base cases
if len(s) <= 1:
return True
# Check first and last characters
if s[0] != s[-1]:
return False
# Recursive call on middle portion
return is_palindrome(s[1:-1])
print(f"'racecar' is palindrome: {is_palindrome('racecar')}") # True
print(f"'python' is palindrome: {is_palindrome('python')}") # False
print(f"'a' is palindrome: {is_palindrome('a')}") # True
# 8. Tower of Hanoi
def tower_of_hanoi(n, source, target, auxiliary):
"""Solve Tower of Hanoi puzzle recursively"""
if n == 1:
print(f"Move disk 1 from {source} to {target}")
return
tower_of_hanoi(n - 1, source, auxiliary, target)
print(f"Move disk {n} from {source} to {target}")
tower_of_hanoi(n - 1, auxiliary, target, source)
print("\nTower of Hanoi (3 disks):")
tower_of_hanoi(3, 'A', 'C', 'B')
# 9. Recursive power function
def power(base, exponent):
"""Calculate base^exponent recursively"""
# Base case
if exponent == 0:
return 1
# Recursive case
return base * power(base, exponent - 1)
print(f"2^5 = {power(2, 5)}") # 32
print(f"3^4 = {power(3, 4)}") # 81
print(f"5^0 = {power(5, 0)}") # 1
# 10. GCD (Greatest Common Divisor) using Euclidean algorithm
def gcd(a, b):
"""Calculate GCD recursively using Euclidean algorithm"""
# Base case
if b == 0:
return a
# Recursive case
return gcd(b, a % b)
print(f"GCD(48, 18) = {gcd(48, 18)}") # 6
print(f"GCD(17, 5) = {gcd(17, 5)}") # 1
# 11. Tail recursion (Python doesn't optimize it, but concept is important)
def factorial_tail(n, accumulator=1):
"""Factorial using tail recursion"""
if n == 0:
return accumulator
return factorial_tail(n - 1, n * accumulator)
print(f"5! (tail recursive) = {factorial_tail(5)}") # 120
# 12. Memoization for recursive functions (optimization)
def fibonacci_memo(n, memo=None):
"""Fibonacci with memoization to avoid repeated calculations"""
if memo is None:
memo = {}
# Check if already computed
if n in memo:
return memo[n]
# Base cases
if n <= 1:
return n
# Compute and store in memo
memo[n] = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo)
return memo[n]
print(f"Fibonacci(40) with memoization = {fibonacci_memo(40)}") # Fast
# Without memoization, fibonacci(40) would take extremely long
# 13. Recursive tree structure
class TreeNode:
def __init__(self, value):
self.value = value
self.children = []
def add_child(self, node):
self.children.append(node)
def __repr__(self, level=0):
ret = " " * level + str(self.value) + "\n"
for child in self.children:
ret += child.__repr__(level + 1)
return ret
# Create a tree
root = TreeNode("Root")
child1 = TreeNode("Child 1")
child2 = TreeNode("Child 2")
grandchild1 = TreeNode("Grandchild 1")
grandchild2 = TreeNode("Grandchild 2")
root.add_child(child1)
root.add_child(child2)
child1.add_child(grandchild1)
child2.add_child(grandchild2)
print("\nTree structure:")
print(root)
Function Practice Exercises
Try these exercises to test your understanding of Python functions.
# Python Functions Practice Exercises
print("=== Exercise 1: Basic Functions ===")
# 1. Create a function that checks if a number is prime
def is_prime(n):
"""Return True if n is prime, False otherwise"""
if n < 2:
return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True
print(f"is_prime(7): {is_prime(7)}") # True
print(f"is_prime(10): {is_prime(10)}") # False
print(f"is_prime(1): {is_prime(1)}") # False
# 2. Create a function that returns the nth Fibonacci number iteratively
def fibonacci_iterative(n):
"""Return nth Fibonacci number without recursion"""
if n <= 0:
return 0
elif n == 1:
return 1
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
print(f"\nFibonacci(10) iterative: {fibonacci_iterative(10)}") # 55
print("\n=== Exercise 2: Function Parameters ===")
# 3. Create a function that accepts any number of arguments and returns their average
def calculate_average(*args):
"""Calculate average of any number of arguments"""
if not args:
return 0
return sum(args) / len(args)
print(f"Average of 1, 2, 3, 4, 5: {calculate_average(1, 2, 3, 4, 5):.2f}") # 3.00
print(f"Average of 10, 20: {calculate_average(10, 20):.2f}") # 15.00
# 4. Create a function with type hints and default parameters
def format_name(first: str, last: str, title: str = "", middle: str = "") -> str:
"""Format a name with optional title and middle name"""
parts = []
if title:
parts.append(title)
parts.append(first)
if middle:
parts.append(middle)
parts.append(last)
return " ".join(parts)
print(f"\nFormatted name: {format_name('John', 'Doe')}")
print(f"Formatted name with title: {format_name('John', 'Doe', title='Dr.')}")
print(f"Formatted full name: {format_name('John', 'Doe', middle='Michael', title='Mr.')}")
print("\n=== Exercise 3: Lambda Functions ===")
# 5. Use lambda with map() to convert temperatures from Celsius to Fahrenheit
celsius_temps = [0, 10, 20, 30, 40]
to_fahrenheit = lambda c: (c * 9/5) + 32
fahrenheit_temps = list(map(to_fahrenheit, celsius_temps))
print(f"Celsius: {celsius_temps}")
print(f"Fahrenheit: {fahrenheit_temps}")
# 6. Use lambda with filter() to find palindromic numbers
numbers = [121, 123, 1331, 456, 555, 78987]
is_palindromic = lambda n: str(n) == str(n)[::-1]
palindromes = list(filter(is_palindromic, numbers))
print(f"\nPalindromic numbers: {palindromes}") # [121, 1331, 555, 78987]
print("\n=== Exercise 4: Decorators ===")
# 7. Create a decorator that validates function arguments
def validate_args(min_val=None, max_val=None):
"""Decorator factory for validating argument ranges"""
def decorator(func):
def wrapper(*args, **kwargs):
# Check positional arguments
for arg in args:
if isinstance(arg, (int, float)):
if min_val is not None and arg < min_val:
raise ValueError(f"Value {arg} below minimum {min_val}")
if max_val is not None and arg > max_val:
raise ValueError(f"Value {arg} above maximum {max_val}")
# Check keyword arguments
for key, value in kwargs.items():
if isinstance(value, (int, float)):
if min_val is not None and value < min_val:
raise ValueError(f"{key}={value} below minimum {min_val}")
if max_val is not None and value > max_val:
raise ValueError(f"{key}={value} above maximum {max_val}")
return func(*args, **kwargs)
return wrapper
return decorator
@validate_args(min_val=0, max_val=100)
def calculate_score(score1, score2):
return (score1 + score2) / 2
try:
result = calculate_score(85, 95)
print(f"Average score: {result}")
# calculate_score(-5, 95) # Would raise ValueError
except ValueError as e:
print(f"Error: {e}")
print("\n=== Exercise 5: Recursion ===")
# 8. Implement recursive function to calculate sum of digits
def sum_digits(n):
"""Return sum of digits of n using recursion"""
if n < 10:
return n
return (n % 10) + sum_digits(n // 10)
print(f"Sum of digits in 12345: {sum_digits(12345)}") # 15 (1+2+3+4+5)
print(f"Sum of digits in 987: {sum_digits(987)}") # 24 (9+8+7)
# 9. Implement recursive binary tree traversal
def count_tree_nodes(node):
"""Count nodes in a binary tree recursively"""
if node is None:
return 0
return 1 + count_tree_nodes(node.left) + count_tree_nodes(node.right)
# Mock tree structure for demonstration
class TreeNode:
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
# Create a sample tree
tree = TreeNode(1,
TreeNode(2,
TreeNode(4),
TreeNode(5)),
TreeNode(3,
TreeNode(6),
TreeNode(7)))
print(f"Number of nodes in tree: {count_tree_nodes(tree)}") # 7
print("\n=== Exercise 6: Advanced Functions ===")
# 10. Create a closure that generates sequential IDs
def id_generator(prefix="ID", start=1):
"""Create a function that generates sequential IDs"""
current = start
def generate():
nonlocal current
id_str = f"{prefix}{current:04d}"
current += 1
return id_str
return generate
# Create different generators
user_id_gen = id_generator("USER", 100)
order_id_gen = id_generator("ORDER", 1)
print("User IDs:", user_id_gen(), user_id_gen(), user_id_gen())
print("Order IDs:", order_id_gen(), order_id_gen(), order_id_gen())
# 11. Create a higher-order function that applies a function n times
def apply_n_times(func, n):
"""Return a function that applies func n times"""
def wrapper(x):
result = x
for _ in range(n):
result = func(result)
return result
return wrapper
double = lambda x: x * 2
double_twice = apply_n_times(double, 2)
double_thrice = apply_n_times(double, 3)
print(f"\nDouble 5 once: {double(5)}") # 10
print(f"Double 5 twice: {double_twice(5)}") # 20 (5*2*2)
print(f"Double 5 thrice: {double_thrice(5)}") # 40 (5*2*2*2)
# 12. Create a function that returns multiple functions
def calculator_factory():
"""Factory that returns multiple calculator functions"""
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
return add, subtract, multiply, divide
add, subtract, multiply, divide = calculator_factory()
print(f"\n10 + 5 = {add(10, 5)}")
print(f"10 - 5 = {subtract(10, 5)}")
print(f"10 * 5 = {multiply(10, 5)}")
print(f"10 / 5 = {divide(10, 5)}")
# 13. Error handling in functions
def safe_divide(numerator, denominator):
"""Divide with proper error handling"""
try:
return numerator / denominator
except ZeroDivisionError:
return float('inf') if numerator > 0 else float('-inf') if numerator < 0 else float('nan')
except TypeError:
raise TypeError("Both arguments must be numbers")
print(f"\nSafe divide 10 / 2: {safe_divide(10, 2)}") # 5.0
print(f"Safe divide 10 / 0: {safe_divide(10, 0)}") # inf
print(f"Safe divide 0 / 0: {safe_divide(0, 0)}") # nan
Key Takeaways
- Functions are defined using the
defkeyword and called with parentheses - Use docstrings (triple quotes) to document functions
- Functions can have parameters (inputs) and return values (outputs)
- Python supports multiple parameter types: positional, keyword, default,
*args,**kwargs - Lambda functions are anonymous, one-line functions:
lambda x: x*2 - Decorators modify function behavior using
@decoratorsyntax - Scope follows LEGB rule: Local → Enclosing → Global → Built-in
- Closures are functions that remember their enclosing scope
- Recursion occurs when a function calls itself (needs base case!)
- Use
globalto modify global variables,nonlocalfor enclosing scope - Type hints (Python 3.5+) improve code readability:
def func(x: int) -> str: - Avoid mutable default arguments (use
Noneinstead) - Pure functions (no side effects) are easier to test and reason about
- Follow the Single Responsibility Principle: each function should do one thing well