Python Functions

Master the art of writing reusable, efficient code with functions

šŸ”§ What are Functions?

Functions are reusable blocks of code that perform specific tasks. They help organize your code, make it more readable, and follow the DRY (Don't Repeat Yourself) principle. Think of functions as mini-programs within your program.


# Simple function example
def greet():
    print("Hello!")

# Function with parameter
def greet_name(name):
    print(f"Hello {name}!")

# Function with return value
def add(a, b):
    return a + b
                                    
def
Keyword
Reusable
Code
Modular
Design

Creating Your First Function

Functions are defined using the def keyword, followed by the function name and parentheses:

Basic Function Definition
# Simple function without parameters
def greet():
    """A simple greeting function"""
    print("Hello, World!")
    print("Welcome to Python functions!")

# Call the function
greet()

# Function with a single parameter
def greet_person(name):
    """Greet a specific person"""
    print(f"Hello, {name}!")
    print(f"Nice to meet you, {name}!")

# Call with different names
greet_person("Alice")
greet_person("Bob")
greet_person("Charlie")

# Function with multiple parameters
def introduce_person(name, age, city):
    """Introduce a person with their details"""
    print(f"Hi! My name is {name}.")
    print(f"I am {age} years old.")
    print(f"I live in {city}.")

# Call with positional arguments
introduce_person("Diana", 28, "New York")
introduce_person("Eve", 32, "London")

šŸ’” Function Anatomy:

  • def : Keyword to define a function
  • function_name : Choose descriptive names
  • (parameters) : Input values (optional)
  • : : Colon to start the function body
  • Indented code : The function's logic

Functions That Return Values

Functions can return values using the return statement, making them more versatile:

Functions with Return Values
# Basic math functions
def add_numbers(a, b):
    """Add two numbers and return the result"""
    result = a + b
    return result

def multiply_numbers(x, y):
    """Multiply two numbers and return the result"""
    return x * y  # Direct return

# Using the functions
sum_result = add_numbers(5, 3)
product_result = multiply_numbers(4, 7)

print(f"5 + 3 = {sum_result}")
print(f"4 Ɨ 7 = {product_result}")

# Function returning multiple values
def get_name_info(full_name):
    """Extract first and last name from full name"""
    parts = full_name.split()
    if len(parts) >= 2:
        first_name = parts[0]
        last_name = parts[-1]
        return first_name, last_name
    else:
        return full_name, ""

# Unpack multiple return values
first, last = get_name_info("John Smith")
print(f"First name: {first}")
print(f"Last name: {last}")

# Function with conditional returns
def calculate_grade(score):
    """Calculate letter grade based on numeric score"""
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"
    elif score >= 70:
        return "C"
    elif score >= 60:
        return "D"
    else:
        return "F"

# Test grade calculation
test_scores = [95, 87, 73, 65, 45]
for score in test_scores:
    grade = calculate_grade(score)
    print(f"Score {score}: Grade {grade}")

# Function returning different data types
def analyze_text(text):
    """Analyze text and return various statistics"""
    word_count = len(text.split())
    char_count = len(text)
    char_count_no_spaces = len(text.replace(" ", ""))
    is_long = len(text) > 50
    
    return {
        "word_count": word_count,
        "character_count": char_count,
        "characters_no_spaces": char_count_no_spaces,
        "is_long_text": is_long,
        "first_word": text.split()[0] if text.split() else "",
        "last_word": text.split()[-1] if text.split() else ""
    }

# Analyze some text
sample_text = "Python functions are powerful tools for organizing code"
analysis = analyze_text(sample_text)

print(f"Text analysis for: '{sample_text}'")
for key, value in analysis.items():
    print(f"  {key}: {value}")

Default Parameters and Keyword Arguments

Make your functions more flexible with default values and keyword arguments:

Flexible Function Parameters
# Functions with default parameters
def greet_with_title(name, title="Mr./Ms."):
    """Greet someone with an optional title"""
    return f"Hello, {title} {name}!"

# Call with and without title
print(greet_with_title("Smith"))           # Uses default title
print(greet_with_title("Johnson", "Dr."))  # Uses custom title

# Multiple default parameters
def create_profile(name, age=25, city="Unknown", occupation="Student"):
    """Create a user profile with default values"""
    profile = {
        "name": name,
        "age": age,
        "city": city,
        "occupation": occupation
    }
    return profile

# Different ways to call the function
profile1 = create_profile("Alice")
profile2 = create_profile("Bob", 30)
profile3 = create_profile("Charlie", city="Paris")
profile4 = create_profile("Diana", age=28, occupation="Engineer", city="Tokyo")

print("Profile 1:", profile1)
print("Profile 2:", profile2)
print("Profile 3:", profile3)
print("Profile 4:", profile4)

# Function with mixed parameter types
def format_message(message, prefix="INFO", timestamp=True, uppercase=False):
    """Format a message with various options"""
    if timestamp:
        from datetime import datetime
        time_str = datetime.now().strftime("%H:%M:%S")
        formatted = f"[{time_str}] {prefix}: {message}"
    else:
        formatted = f"{prefix}: {message}"
    
    if uppercase:
        formatted = formatted.upper()
    
    return formatted

# Test different combinations
print(format_message("System started"))
print(format_message("Warning detected", prefix="WARN"))
print(format_message("Error occurred", prefix="ERROR", uppercase=True))
print(format_message("Debug info", prefix="DEBUG", timestamp=False))

# Keyword-only arguments (Python 3+)
def calculate_rectangle_area(*, length, width, unit="sq units"):
    """Calculate rectangle area with keyword-only arguments"""
    area = length * width
    return f"{area} {unit}"

# Must use keyword arguments
# calculate_rectangle_area(5, 3)  # This would cause an error
area1 = calculate_rectangle_area(length=5, width=3)
area2 = calculate_rectangle_area(length=10, width=7, unit="sq meters")

print(f"Rectangle area 1: {area1}")
print(f"Rectangle area 2: {area2}")

Variable Arguments (*args and **kwargs)

Handle functions with variable numbers of arguments using *args and **kwargs:

Variable Arguments
# *args - Variable positional arguments
def calculate_sum(*numbers):
    """Calculate sum of any number of arguments"""
    total = 0
    for num in numbers:
        total += num
    return total

# Call with different numbers of arguments
print(f"Sum of 1, 2, 3: {calculate_sum(1, 2, 3)}")
print(f"Sum of 10, 20: {calculate_sum(10, 20)}")
print(f"Sum of 1, 2, 3, 4, 5: {calculate_sum(1, 2, 3, 4, 5)}")

def find_maximum(*values):
    """Find maximum value from any number of arguments"""
    if not values:
        return None
    
    max_val = values[0]
    for val in values[1:]:
        if val > max_val:
            max_val = val
    return max_val

print(f"Maximum of 3, 7, 2, 9, 1: {find_maximum(3, 7, 2, 9, 1)}")

# **kwargs - Variable keyword arguments
def create_student_record(**student_info):
    """Create a student record from keyword arguments"""
    record = {"type": "student"}
    record.update(student_info)
    return record

# Call with different keyword arguments
student1 = create_student_record(name="Alice", age=20, major="Computer Science")
student2 = create_student_record(name="Bob", age=22, major="Mathematics", gpa=3.8)
student3 = create_student_record(name="Charlie", age=19, major="Physics", 
                                year="Sophomore", scholarship=True)

print("Student 1:", student1)
print("Student 2:", student2)
print("Student 3:", student3)

# Combining regular parameters, *args, and **kwargs
def process_order(customer_name, *items, **options):
    """Process an order with customer name, items, and options"""
    print(f"Processing order for: {customer_name}")
    print(f"Items ordered: {', '.join(items)}")
    
    if options:
        print("Special options:")
        for key, value in options.items():
            print(f"  {key}: {value}")
    
    total_items = len(items)
    return f"Order processed: {total_items} items for {customer_name}"

# Test the flexible function
result1 = process_order("John", "Pizza", "Soda")
print(result1)
print()

result2 = process_order("Jane", "Burger", "Fries", "Shake", 
                       delivery=True, tip=5.00, special_instructions="Extra sauce")
print(result2)
print()

# Advanced: Function that accepts and forwards arguments
def logged_function_call(func, *args, **kwargs):
    """Call a function and log the call details"""
    print(f"Calling function: {func.__name__}")
    print(f"Arguments: {args}")
    print(f"Keyword arguments: {kwargs}")
    
    result = func(*args, **kwargs)
    
    print(f"Function returned: {result}")
    return result

# Test with our previous functions
print("=== Logged Function Calls ===")
logged_function_call(calculate_sum, 1, 2, 3, 4)
print()
logged_function_call(create_student_record, name="Test Student", age=21, major="Testing")

Advanced Function Concepts

Explore advanced concepts like nested functions, closures, and decorators:

Advanced Function Concepts
# Nested functions and closures
def create_multiplier(factor):
    """Create a function that multiplies by a specific factor"""
    def multiply(number):
        return number * factor
    return multiply

# Create specialized multiplier functions
double = create_multiplier(2)
triple = create_multiplier(3)
times_ten = create_multiplier(10)

print(f"Double 5: {double(5)}")
print(f"Triple 7: {triple(7)}")
print(f"Times ten 3: {times_ten(3)}")

# Function as first-class objects
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):
    return a / b if b != 0 else "Cannot divide by zero"

# Store functions in a dictionary
operations = {
    "+": add,
    "-": subtract,
    "*": multiply,
    "/": divide
}

def calculate(a, operator, b):
    """Perform calculation using function lookup"""
    if operator in operations:
        return operations[operator](a, b)
    else:
        return "Unknown operator"

# Test the calculator
print(f"10 + 5 = {calculate(10, '+', 5)}")
print(f"10 - 3 = {calculate(10, '-', 3)}")
print(f"4 * 7 = {calculate(4, '*', 7)}")
print(f"15 / 3 = {calculate(15, '/', 3)}")

# Simple decorator example
def timing_decorator(func):
    """Decorator to measure function execution time"""
    import time
    
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        execution_time = end_time - start_time
        print(f"Function {func.__name__} took {execution_time:.4f} seconds")
        return result
    
    return wrapper

# Apply decorator
@timing_decorator
def slow_function():
    """A function that takes some time to execute"""
    import time
    time.sleep(0.1)  # Simulate slow operation
    return "Task completed"

# Test decorated function
result = slow_function()
print(f"Result: {result}")

# Recursive functions
def factorial(n):
    """Calculate factorial using recursion"""
    if n <= 1:
        return 1
    else:
        return n * factorial(n - 1)

def fibonacci(n):
    """Calculate Fibonacci number using recursion"""
    if n <= 1:
        return n
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)

# Test recursive functions
print(f"Factorial of 5: {factorial(5)}")
print(f"Fibonacci sequence (first 10 numbers):")
for i in range(10):
    print(f"F({i}) = {fibonacci(i)}")

# Lambda functions (anonymous functions)
# Simple lambda examples
square = lambda x: x ** 2
add_ten = lambda x: x + 10
is_even = lambda x: x % 2 == 0

print(f"Square of 6: {square(6)}")
print(f"Add 10 to 15: {add_ten(15)}")
print(f"Is 8 even? {is_even(8)}")

# Using lambda with built-in functions
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Filter even numbers
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(f"Even numbers: {even_numbers}")

# Square all numbers
squared_numbers = list(map(lambda x: x ** 2, numbers))
print(f"Squared numbers: {squared_numbers}")

# Sort by custom criteria
students = [
    {"name": "Alice", "grade": 85},
    {"name": "Bob", "grade": 92},
    {"name": "Charlie", "grade": 78},
    {"name": "Diana", "grade": 96}
]

# Sort by grade (descending)
sorted_students = sorted(students, key=lambda student: student["grade"], reverse=True)
print("Students sorted by grade (highest first):")
for student in sorted_students:
    print(f"  {student['name']}: {student['grade']}")

Real-World Example: Task Management System

Here's a comprehensive example showing how functions work together in a real application:

Task Management System
"""
Task Management System
Demonstrates comprehensive use of Python functions
"""

from datetime import datetime, timedelta
import json

# Global task storage
tasks = []
task_id_counter = 1

def generate_task_id():
    """Generate unique task ID"""
    global task_id_counter
    current_id = task_id_counter
    task_id_counter += 1
    return current_id

def create_task(title, description="", priority="medium", due_date=None):
    """Create a new task with specified parameters"""
    task = {
        "id": generate_task_id(),
        "title": title,
        "description": description,
        "priority": priority,
        "status": "pending",
        "created_at": datetime.now().isoformat(),
        "due_date": due_date,
        "completed_at": None
    }
    tasks.append(task)
    return task

def get_task_by_id(task_id):
    """Find and return a task by its ID"""
    for task in tasks:
        if task["id"] == task_id:
            return task
    return None

def update_task(task_id, **updates):
    """Update task fields using keyword arguments"""
    task = get_task_by_id(task_id)
    if task:
        for key, value in updates.items():
            if key in task:
                task[key] = value
        return task
    return None

def complete_task(task_id):
    """Mark a task as completed"""
    task = get_task_by_id(task_id)
    if task:
        task["status"] = "completed"
        task["completed_at"] = datetime.now().isoformat()
        return True
    return False

def delete_task(task_id):
    """Delete a task by ID"""
    global tasks
    tasks = [task for task in tasks if task["id"] != task_id]

def filter_tasks(status=None, priority=None, overdue_only=False):
    """Filter tasks based on various criteria"""
    filtered = tasks.copy()
    
    if status:
        filtered = [task for task in filtered if task["status"] == status]
    
    if priority:
        filtered = [task for task in filtered if task["priority"] == priority]
    
    if overdue_only:
        current_time = datetime.now()
        filtered = [
            task for task in filtered 
            if task["due_date"] and 
            datetime.fromisoformat(task["due_date"]) < current_time and
            task["status"] != "completed"
        ]
    
    return filtered

def sort_tasks(tasks_list, sort_by="created_at", reverse=False):
    """Sort tasks by specified field"""
    if sort_by == "priority":
        priority_order = {"high": 3, "medium": 2, "low": 1}
        return sorted(tasks_list, 
                     key=lambda task: priority_order.get(task["priority"], 0), 
                     reverse=reverse)
    else:
        return sorted(tasks_list, key=lambda task: task.get(sort_by, ""), reverse=reverse)

def get_task_statistics():
    """Calculate and return task statistics"""
    total_tasks = len(tasks)
    completed_tasks = len([task for task in tasks if task["status"] == "completed"])
    pending_tasks = total_tasks - completed_tasks
    
    priority_counts = {"high": 0, "medium": 0, "low": 0}
    for task in tasks:
        priority = task.get("priority", "medium")
        priority_counts[priority] += 1
    
    # Calculate overdue tasks
    current_time = datetime.now()
    overdue_tasks = len([
        task for task in tasks 
        if task["due_date"] and 
        datetime.fromisoformat(task["due_date"]) < current_time and
        task["status"] != "completed"
    ])
    
    return {
        "total": total_tasks,
        "completed": completed_tasks,
        "pending": pending_tasks,
        "overdue": overdue_tasks,
        "by_priority": priority_counts,
        "completion_rate": (completed_tasks / total_tasks * 100) if total_tasks > 0 else 0
    }

def format_task_display(task, show_details=False):
    """Format task for display"""
    status_icon = "āœ…" if task["status"] == "completed" else "ā³"
    priority_icon = {"high": "šŸ”“", "medium": "🟔", "low": "🟢"}.get(task["priority"], "⚪")
    
    basic_info = f"{status_icon} [{task['id']}] {task['title']} {priority_icon}"
    
    if show_details:
        details = []
        if task["description"]:
            details.append(f"Description: {task['description']}")
        if task["due_date"]:
            due_date = datetime.fromisoformat(task["due_date"])
            details.append(f"Due: {due_date.strftime('%Y-%m-%d %H:%M')}")
        details.append(f"Priority: {task['priority'].title()}")
        details.append(f"Status: {task['status'].title()}")
        
        if details:
            return basic_info + "\n    " + "\n    ".join(details)
    
    return basic_info

def export_tasks_to_json(filename="tasks.json"):
    """Export tasks to JSON file"""
    try:
        with open(filename, 'w') as file:
            json.dump(tasks, file, indent=2)
        return f"Tasks exported to {filename}"
    except Exception as e:
        return f"Error exporting tasks: {e}"

def import_tasks_from_json(filename="tasks.json"):
    """Import tasks from JSON file"""
    global tasks, task_id_counter
    try:
        with open(filename, 'r') as file:
            imported_tasks = json.load(file)
        
        tasks.extend(imported_tasks)
        
        # Update task ID counter
        if tasks:
            max_id = max(task["id"] for task in tasks)
            task_id_counter = max_id + 1
        
        return f"Imported {len(imported_tasks)} tasks from {filename}"
    except FileNotFoundError:
        return f"File {filename} not found"
    except Exception as e:
        return f"Error importing tasks: {e}"

# Demo the task management system
def demo_task_system():
    """Demonstrate the task management system"""
    print("šŸ—‚ļø  TASK MANAGEMENT SYSTEM DEMO")
    print("=" * 50)
    
    # Create some sample tasks
    task1 = create_task(
        "Complete Python tutorial", 
        "Finish learning about functions",
        priority="high",
        due_date=(datetime.now() + timedelta(days=2)).isoformat()
    )
    
    task2 = create_task(
        "Buy groceries",
        "Milk, bread, eggs, fruits",
        priority="medium",
        due_date=(datetime.now() + timedelta(days=1)).isoformat()
    )
    
    task3 = create_task(
        "Exercise",
        "30 minutes cardio workout",
        priority="low"
    )
    
    task4 = create_task(
        "Submit report",
        "Monthly progress report",
        priority="high",
        due_date=(datetime.now() - timedelta(days=1)).isoformat()  # Overdue
    )
    
    print("šŸ“ Created sample tasks:")
    for task in tasks:
        print(f"  {format_task_display(task)}")
    
    # Complete a task
    print(f"\nāœ… Completing task {task2['id']}...")
    complete_task(task2["id"])
    
    # Update a task
    print(f"\nšŸ“ Updating task {task3['id']}...")
    update_task(task3["id"], description="45 minutes workout including weights")
    
    # Show statistics
    print(f"\nšŸ“Š TASK STATISTICS:")
    stats = get_task_statistics()
    print(f"  Total tasks: {stats['total']}")
    print(f"  Completed: {stats['completed']}")
    print(f"  Pending: {stats['pending']}")
    print(f"  Overdue: {stats['overdue']}")
    print(f"  Completion rate: {stats['completion_rate']:.1f}%")
    print(f"  Priority breakdown: {stats['by_priority']}")
    
    # Show filtered tasks
    print(f"\nšŸ” HIGH PRIORITY TASKS:")
    high_priority = filter_tasks(priority="high")
    for task in high_priority:
        print(f"  {format_task_display(task, show_details=True)}")
    
    print(f"\nāš ļø  OVERDUE TASKS:")
    overdue = filter_tasks(overdue_only=True)
    for task in overdue:
        print(f"  {format_task_display(task, show_details=True)}")
    
    # Show sorted tasks
    print(f"\nšŸ“‹ ALL TASKS (sorted by priority):")
    sorted_tasks = sort_tasks(tasks, sort_by="priority", reverse=True)
    for task in sorted_tasks:
        print(f"  {format_task_display(task)}")

# Run the demo
if __name__ == "__main__":
    demo_task_system()

🧠 Test Your Knowledge

Test your understanding of Python functions:

Question 1: Which keyword is used to define a function in Python?

Question 2: What does *args allow in a function?

Question 3: What happens if a function doesn't have a return statement?