11  Creating Functions - Build Your Own Python Tools

11.1 Chapter Outline

  • Understanding the importance of custom functions
  • The function definition syntax and best practices
  • Working with parameters and arguments effectively
  • Return values and how to use them
  • Understanding variable scope and lifetime
  • Designing functions for reusability and maintainability
  • Using functions to structure your chatbot project

11.2 Learning Objectives

By the end of this chapter, you will be able to: - Create well-designed Python functions using the def keyword - Implement functions with flexible parameter structures - Return and process values from functions - Understand and work within function scope rules - Apply best practices for function design - Structure your chatbot project using custom functions - Debug and refine your functions

11.3 1. Introduction: Function Creation as a Superpower

In the previous chapter, we learned how to use built-in and library functions. Now, we’ll unlock one of Python’s most powerful features: the ability to create your own custom functions. This is where programming truly becomes creative – you’re no longer limited to what others have built; you can craft tools perfectly tailored to your specific needs.

Creating functions is like gaining a superpower that allows you to:

  • Organize your code into logical, self-contained units
  • Reuse code without copying and pasting (DRY - Don’t Repeat Yourself)
  • Abstract complex operations behind simple interfaces
  • Focus on one problem at a time
  • Test discrete pieces of functionality
  • Collaborate by defining clear boundaries in code

Key Concept: Functions transform programming from writing sequential instructions to designing components that work together. This “modular” approach is how all sophisticated software is built, from web browsers to AI systems.

For our chatbot project, custom functions will take us from a linear script to a well-structured program that’s easier to understand, debug, and extend. Creating functions is the first step toward proper software architecture.

11.4 2. Function Definition: The Building Blocks

A function in Python consists of a header (the definition line) and a body (the indented code block that follows):

def function_name(parameters):
    """Docstring: explains what the function does."""
    # Function body - indented code
    # that runs when the function is called
    return result  # Optional return statement

Let’s break down each component:

  • def: The keyword that tells Python you’re defining a function
  • function_name: A name you choose (following variable naming conventions)
  • parameters: Optional inputs the function can accept (in parentheses)
  • docstring: Optional (but recommended) documentation in triple quotes
  • Function body: The indented code block that executes when called
  • return: Optional statement to send a value back to the caller

Here’s a simple example:

def greet():
    """Display a simple greeting."""
    print("Hello, world!")

# Calling the function
greet()  # Output: Hello, world!

Every time you call the function, its code executes:

greet()  # Hello, world!
greet()  # Hello, world!
greet()  # Hello, world!

11.4.1 Function Naming Best Practices

Good function names are:

  • Descriptive of what the function does
  • Verb-focused since functions perform actions (e.g., calculate_total, not just total)
  • Lowercase with underscores between words (snake_case)
  • Consistent in style throughout your program
# Good function names
def calculate_area(width, height):
    return width * height

def validate_email(email):
    return "@" in email and "." in email

def get_user_choice():
    return input("Enter your choice: ")

# Less helpful function names
def calc(w, h):  # Too vague and abbreviated
    return w * h

def function1():  # Says nothing about purpose
    print("Hello")

11.5 3. Parameters and Arguments: Making Functions Flexible

Parameters make functions adaptable by allowing them to work with different inputs each time they’re called:

def greet(name):
    """Greet a person by name."""
    print(f"Hello, {name}!")

# Calling with different arguments
greet("Alice")  # Output: Hello, Alice!
greet("Bob")    # Output: Hello, Bob!

11.5.1 Required Parameters

In the example above, name is a required parameter. If you don’t provide it, Python raises an error:

greet()  # Error: greet() missing 1 required positional argument: 'name'

11.5.2 Multiple Parameters

Functions can accept multiple parameters:

def display_info(name, age, location):
    """Display a person's information."""
    print(f"Name: {name}")
    print(f"Age: {age}")
    print(f"Location: {location}")

# Call with multiple arguments
display_info("Charlie", 25, "San Francisco")

When calling functions with multiple parameters, you must provide them in the correct order, or use named arguments:

# Positional arguments (order matters)
display_info("Charlie", 25, "San Francisco")

# Named arguments (order doesn't matter)
display_info(location="San Francisco", name="Charlie", age=25)

# Mix of positional and named (positional must come first)
display_info("Charlie", location="San Francisco", age=25)

11.5.3 Default Parameter Values

You can make parameters optional by providing default values:

def greet(name, greeting="Hello"):
    """Greet a person with a customizable greeting."""
    print(f"{greeting}, {name}!")

# Using the default greeting
greet("Diana")  # Output: Hello, Diana!

# Overriding the default
greet("Edward", "Good morning")  # Output: Good morning, Edward!

Default parameters must come after non-default parameters:

# Correct: default parameter after required parameter
def function(required, optional="default"):
    pass

# Incorrect: default parameter before required parameter
def function(optional="default", required):  # SyntaxError
    pass

11.5.4 Parameter Types and Docstrings

Good practice includes documenting the expected types and purpose of parameters:

def calculate_total(price, quantity, tax_rate=0.08):
    """
    Calculate the total cost including tax.

    Args:
        price (float): The price per item
        quantity (int): The number of items
        tax_rate (float, optional): The tax rate as a decimal. Defaults to 0.08.

    Returns:
        float: The total cost including tax
    """
    subtotal = price * quantity
    tax = subtotal * tax_rate
    return subtotal + tax

This documentation helps other developers (and future you) understand how to use the function correctly.

11.6 4. Return Values: Getting Results from Functions

Functions can send data back to the caller using the return statement:

def add(a, b):
    """Add two numbers and return the result."""
    return a + b

# Using the return value
sum_result = add(5, 3)
print(sum_result)  # Output: 8

# Using the return value directly in an expression
total = add(5, 3) + add(2, 1)
print(total)  # Output: 11

The return statement immediately exits the function and sends the specified value back:

def absolute_value(number):
    """Return the absolute value of a number."""
    if number >= 0:
        return number  # Function ends here if number is positive

    # This code only runs if the number is negative
    return -number

print(absolute_value(5))    # Output: 5
print(absolute_value(-10))  # Output: 10

11.6.1 Returning Multiple Values

Python functions can return multiple values using tuples:

def get_dimensions():
    """Return width and height."""
    width = 800
    height = 600
    return width, height  # Returns a tuple (800, 600)

# Unpacking the returned tuple
screen_width, screen_height = get_dimensions()
print(f"Width: {screen_width}, Height: {screen_height}")

11.6.2 No Return Statement

If a function doesn’t include a return statement, Python implicitly returns None:

def greet(name):
    """Greet a person by name."""
    print(f"Hello, {name}!")

# This function doesn't explicitly return anything
result = greet("Frank")
print(result)  # Output: None

This behavior explains why print() doesn’t appear to return anything - it’s returning None after displaying output.

11.6.3 Return Early for Validation

Returning early is useful for validation:

def divide(a, b):
    """Divide a by b, with validation."""
    # Validate input first
    if b == 0:
        print("Error: Cannot divide by zero")
        return None  # Return early on invalid input

    # Only execute if validation passed
    return a / b

11.7 5. Variable Scope: Understanding Where Variables Live

When you create a variable inside a function, it only exists within that function. This is called “local scope”:

def show_local():
    """Demonstrate local variable scope."""
    # This variable only exists inside this function
    message = "I'm a local variable"
    print(message)

show_local()  # Output: I'm a local variable
# print(message)  # Error: name 'message' is not defined

Parameters are also local variables:

def double(number):  # 'number' is a local variable
    return number * 2

result = double(5)
# print(number)  # Error: 'number' doesn't exist outside the function

11.7.1 Local vs. Global Scope

Variables defined outside any function have “global scope” and can be accessed (but not modified) inside functions:

# Global variable
counter = 0

def increment():
    """Try to increment the counter."""
    # This creates a new local variable, not modifying the global one
    counter = counter + 1  # UnboundLocalError
    return counter

# To modify a global variable:
def correct_increment():
    """Increment the global counter."""
    global counter  # Declare that we want to use the global variable
    counter = counter + 1
    return counter

However, it’s generally better practice to pass values as parameters and return results, rather than relying on global variables:

# Better approach
def increment(value):
    """Increment the given value."""
    return value + 1

counter = 0
counter = increment(counter)  # counter becomes 1

11.7.2 Variable Lifetime

Local variables are created when the function is called and destroyed when it returns:

def process_data():
    temp = 100  # Created when function starts
    print(f"Processing at {temp}")
    # 'temp' is destroyed when function ends

process_data()
process_data()  # Creates a fresh 'temp' variable

This explains why functions don’t “remember” values from previous calls unless you use global variables or other techniques we’ll learn later.

11.8 6. Designing Effective Functions

Well-designed functions follow several principles:

11.8.1 1. Single Responsibility Principle

Each function should have one clear purpose:

# Too many responsibilities
def process_user(name):
    print(f"Processing user: {name}")
    validate_email(f"{name}@example.com")
    create_account(name)
    send_welcome_email(name)

# Better: separate functions for each responsibility
def validate_user_email(email):
    """Check if email is valid."""
    return "@" in email and "." in email

def create_user_account(username):
    """Create account in the system."""
    print(f"Creating account for {username}")

def send_welcome_email(email):
    """Send welcome message to user."""
    print(f"Sending welcome email to {email}")

11.8.2 2. Keep Functions Short and Focused

Aim for functions that fit on one screen (20-30 lines maximum):

# Too long and complex
def do_everything():
    # 100 lines of code doing many different things
    pass

# Better: break into smaller functions
def validate_input():
    # 10 lines of code
    pass

def process_data():
    # 15 lines of code
    pass

def generate_report():
    # 20 lines of code
    pass

11.8.3 3. Use Descriptive Names and Documentation

Make your functions self-documenting:

# Unclear
def calc(a, b):
    return a * b

# Better
def calculate_rectangle_area(width, height):
    """
    Calculate the area of a rectangle.

    Args:
        width (float): Width of the rectangle
        height (float): Height of the rectangle

    Returns:
        float: Area of the rectangle
    """
    return width * height

11.8.4 4. Minimize Side Effects

Functions should ideally return values rather than modifying global state:

# Function with side effects
total = 0
def add_to_total(value):
    global total
    total += value

# Better: pure function
def add(a, b):
    return a + b

total = add(total, value)  # Explicit about what's changing

11.8.5 5. Error Handling

Consider what happens when things go wrong:

def divide(a, b):
    """
    Divide a by b.

    Args:
        a (float): Numerator
        b (float): Denominator (must be non-zero)

    Returns:
        float: Result of a/b, or None if division by zero attempted
    """
    if b == 0:
        print("Error: Cannot divide by zero")
        return None
    return a / b

11.9 7. Project Corner: Structuring Your Chatbot with Functions

Now let’s apply these principles to enhance our chatbot project with well-designed functions. Previously, we had a simple script; now we’ll organize it into modular, reusable components.

11.9.1 Basic Function-Based Chatbot

def get_response(user_input, user_name):
    """
    Generate a response based on user input.

    Args:
        user_input (str): The user's message
        user_name (str): The user's name

    Returns:
        str: The chatbot's response
    """
    user_input = user_input.lower()

    if "hello" in user_input or "hi" in user_input:
        return f"Hello there, {user_name}!"
    elif "how are you" in user_input:
        return "I'm just a computer program, but thanks for asking!"
    elif "your name" in user_input:
        return f"My name is PyBot. I'm here to help you learn Python!"
    elif "bye" in user_input or "goodbye" in user_input:
        return f"Goodbye, {user_name}! Have a great day!"
    else:
        return "I'm not sure how to respond to that yet."

def run_chatbot():
    """Run the main chatbot interaction loop."""
    bot_name = "PyBot"
    print(f"Hello! I'm {bot_name}. Type 'bye' to exit.")

    user_name = input("What's your name? ")
    print(f"Nice to meet you, {user_name}!")

    while True:
        user_input = input(f"{user_name}> ")
        if user_input.lower() == "bye":
            print(f"{bot_name}> Goodbye, {user_name}!")
            break

        response = get_response(user_input, user_name)
        print(f"{bot_name}> {response}")

# Run the chatbot if this file is executed directly
if __name__ == "__main__":
    run_chatbot()

This approach already improves our code by: - Separating response generation into a dedicated function - Adding proper documentation with docstrings - Creating a main function that handles the chatbot loop - Using a conditional to only run the chatbot when the file is executed directly

11.9.2 Enhanced Modular Chatbot

Let’s take it further by adding more functions for specific tasks:

def get_user_name():
    """
    Get the user's name with basic validation.

    Returns:
        str: The user's name
    """
    while True:
        name = input("What's your name? ").strip()
        if name:  # Check that name isn't empty
            return name
        print("I didn't catch that. Please tell me your name.")

def display_welcome(bot_name):
    """
    Display the welcome message.

    Args:
        bot_name (str): The chatbot's name
    """
    print("\n" + "=" * 50)
    print(f"Welcome to {bot_name}!")
    print("=" * 50)
    print(f"Hello! I'm {bot_name}, a simple chatbot.")
    print("I can help you learn about Python functions.")
    print("Type 'bye' to exit, 'help' for commands.\n")

def get_user_input(user_name):
    """
    Get input from the user with their name as prompt.

    Args:
        user_name (str): The user's name

    Returns:
        str: The user's input
    """
    return input(f"{user_name}> ").strip()

def display_response(bot_name, response):
    """
    Display the chatbot's response.

    Args:
        bot_name (str): The chatbot's name
        response (str): The response to display
    """
    print(f"{bot_name}> {response}")

def get_response(user_input, user_name):
    """
    Generate a response based on user input.

    Args:
        user_input (str): The user's message
        user_name (str): The user's name

    Returns:
        str: The chatbot's response
    """
    user_input = user_input.lower()

    # Check for specific commands
    if user_input == "help":
        return get_help_message()

    # Check for greetings
    if any(greeting in user_input for greeting in ["hello", "hi", "hey"]):
        return f"Hello there, {user_name}!"

    # Check for questions about the bot
    if "your name" in user_input:
        return "My name is PyBot. I'm a simple chatbot built with Python functions!"

    if "how are you" in user_input:
        return "I'm just a computer program, but I'm functioning well. Thanks for asking!"

    # Check for farewells
    if any(farewell in user_input for farewell in ["bye", "goodbye", "exit"]):
        return f"Goodbye, {user_name}! Have a great day!"

    # Default response
    return "I'm not sure how to respond to that yet. Type 'help' for commands."

def get_help_message():
    """
    Return the help message.

    Returns:
        str: The help message
    """
    return """
I understand the following:
- Greetings (hello, hi)
- Questions about me
- 'how are you'
- 'bye' or 'goodbye' to exit
"""

def run_chatbot():
    """Run the main chatbot interaction loop."""
    bot_name = "PyBot"

    display_welcome(bot_name)
    user_name = get_user_name()
    print(f"\n{bot_name}> Nice to meet you, {user_name}!\n")

    while True:
        user_input = get_user_input(user_name)

        # Check for exit command
        if user_input.lower() == "bye":
            display_response(bot_name, f"Goodbye, {user_name}!")
            break

        response = get_response(user_input, user_name)
        display_response(bot_name, response)

# Run the chatbot if this file is executed directly
if __name__ == "__main__":
    run_chatbot()

This enhanced version demonstrates several advanced function design principles:

  1. Each function has a single responsibility - input, output, logic, etc.
  2. Functions are composable - the main function calls smaller functions
  3. Error handling is built in - input validation in get_user_name()
  4. Code is self-documenting - function names clearly describe their purpose
  5. Helper functions hide complexity - like get_help_message()

11.9.3 Adding New Capabilities with Functions

When you want to extend your chatbot, you can simply add new functions:

def calculate_age(birth_year):
    """
    Calculate age based on birth year.

    Args:
        birth_year (int): The year the person was born

    Returns:
        int: The calculated age (approximate)
    """
    from datetime import datetime
    current_year = datetime.now().year
    return current_year - birth_year

def handle_age_question(user_input):
    """
    Extract birth year from input and calculate age.

    Args:
        user_input (str): The user's message

    Returns:
        str: Age calculation response or None if not an age question
    """
    if "calculate age" in user_input.lower() or "how old" in user_input.lower():
        try:
            # Try to extract a year from the input
            words = user_input.split()
            for word in words:
                if word.isdigit() and 1900 <= int(word) <= 2020:
                    age = calculate_age(int(word))
                    return f"If you were born in {word}, you are approximately {age} years old now."

            # If no year found, ask for it
            return "I can calculate age. What year were you born?"
        except:
            return "I couldn't calculate that age. Try specifying a birth year between 1900 and 2020."

    return None  # Not an age question

# Then in get_response():
def get_response(user_input, user_name):
    # Existing response logic...

    # Check for age-related questions
    age_response = handle_age_question(user_input)
    if age_response:
        return age_response

    # Other response logic...

This modular approach makes extending your chatbot clean and manageable. Each new capability goes into its own function, keeping your code organized.

11.10 8. AI Collaboration Corner: Designing Better Functions

Let’s explore how AI assistants can help you create better functions in your Python code.

11.10.1 Before: Vague Prompting

Help me create a function in Python.

AI Response: To create a function in Python, you use the def keyword followed by the function name and parentheses. Here’s a basic example:

def greet(name):
    """This function greets the person passed in as a parameter."""
    print(f"Hello, {name}!")

greet("Alice")  # Output: Hello, Alice!

Functions can have parameters, return values, and docstrings that explain what they do. Parameters make your functions reusable with different inputs.

This response provides basic information but doesn’t address specific function design needs.

11.10.2 After: Detailed, Specific Prompting

I'm building a chatbot that needs to process user input and determine sentiment
(positive/negative/neutral). I want to create a well-designed function that:

1. Takes a message string as input
2. Analyzes the text for positive and negative keywords
3. Returns both a sentiment label and a confidence score

Please provide a function with proper documentation, parameter validation, and
error handling. Also explain any design decisions in your implementation.

AI Response: Here’s a well-designed function to analyze sentiment in chatbot messages:

def analyze_sentiment(message, custom_keywords=None):
    """
    Analyze the sentiment of a message as positive, negative, or neutral.

    Args:
        message (str): The user message to analyze
        custom_keywords (dict, optional): Dictionary with custom 'positive' and
                                         'negative' keyword lists to use instead
                                         of the defaults

    Returns:
        tuple: (sentiment, confidence) where:
            - sentiment (str): 'positive', 'negative', or 'neutral'
            - confidence (float): Score between 0.0 and 1.0 indicating confidence

    Raises:
        TypeError: If message is not a string or custom_keywords is not a dict/None
        ValueError: If message is empty
    """
    # Validate inputs
    if not isinstance(message, str):
        raise TypeError("Message must be a string")

    if custom_keywords is not None and not isinstance(custom_keywords, dict):
        raise TypeError("Custom keywords must be a dictionary")

    if not message.strip():
        raise ValueError("Message cannot be empty")

    # Prepare the message for analysis
    message = message.lower()
    words = message.split()

    # Default sentiment keywords
    default_keywords = {
        'positive': ['happy', 'glad', 'good', 'great', 'excellent', 'wonderful',
                     'amazing', 'love', 'like', 'thanks', 'thank', 'awesome'],
        'negative': ['bad', 'sad', 'terrible', 'awful', 'horrible', 'hate',
                     'dislike', 'sorry', 'disappointed', 'unhappy', 'wrong']
    }

    # Use custom keywords if provided, otherwise use defaults
    keywords = custom_keywords if custom_keywords else default_keywords

    # Count sentiment matches
    positive_matches = sum(1 for word in words if word in keywords['positive'])
    negative_matches = sum(1 for word in words if word in keywords['negative'])
    total_sentiment_words = positive_matches + negative_matches

    # Calculate sentiment and confidence
    if total_sentiment_words == 0:
        return 'neutral', 0.5  # No sentiment words found

    if positive_matches > negative_matches:
        sentiment = 'positive'
        confidence = positive_matches / total_sentiment_words
    elif negative_matches > positive_matches:
        sentiment = 'negative'
        confidence = negative_matches / total_sentiment_words
    else:
        sentiment = 'neutral'
        confidence = 0.5

    # Adjust confidence to be between 0.5 and 1.0 (0.5 = neutral)
    confidence = 0.5 + (confidence * 0.5)

    return sentiment, confidence

# Example usage:
def demo_sentiment_analysis():
    """Run examples of sentiment analysis."""
    test_messages = [
        "I'm really happy with your service, thank you!",
        "This is absolutely terrible and disappointing.",
        "The weather is cloudy today."
    ]

    for message in test_messages:
        sentiment, confidence = analyze_sentiment(message)
        print(f"Message: '{message}'")
        print(f"Sentiment: {sentiment}, Confidence: {confidence:.2f}\n")

Design decisions explained:

  1. Function signature:
    • Primary parameter (message) for the text to analyze
    • Optional parameter (custom_keywords) for flexibility/customization
    • Return value is a tuple with both result and confidence score
  2. Robust validation:
    • Type checking for parameters
    • Empty message validation
    • Explicit error messages for troubleshooting
  3. Well-structured implementation:
    • Message preprocessing (lowercase, word splitting)
    • Default keywords with option for customization
    • Explicit algorithm for sentiment determination
    • Confidence scoring scaled to a meaningful range
  4. Complete documentation:
    • Clear description of function purpose
    • Detailed parameter documentation
    • Return value format explanation
    • Documentation of potential errors
  5. Design principle: Single Responsibility:
    • The function does one thing well (sentiment analysis)
    • Doesn’t mix other concerns like input/output

This function demonstrates good practices like thorough validation, flexible parameters, meaningful return values, and comprehensive documentation. It’s also designed to be extended easily (e.g., by providing custom keywords).

11.10.3 How to Craft Effective Prompts for Function Design

When working with an AI assistant on function design:

  1. Specify the exact purpose of the function you need

  2. Describe inputs and outputs clearly

  3. Mention any special requirements like error handling or validation

  4. Ask about design considerations specific to your use case

  5. Request documentation if you want it included

For example:

I need to create a function that validates user registration data with these requirements:
1. It should check username (letters/numbers only, 3-15 chars), email (must have @ and .),
   and password (8+ chars with at least one number)
2. For invalid data, it should return specific error messages about what's wrong
3. For valid data, it should return a success message
4. The function should be well-documented and follow Python best practices

Please provide the function code with comments explaining the implementation.

This specific prompt will yield a well-designed validation function tailored to your exact requirements.

11.11 9. Common Function Design Pitfalls to Avoid

As you create your own functions, watch for these common issues:

11.11.1 Too Many Parameters

# Hard to use and understand
def create_user(name, email, password, age, location, interests, account_type,
                newsletter, referral_code, language, theme_preference):
    # Function with too many parameters
    pass

# Better approach
def create_user(name, email, password, **optional_fields):
    # Core parameters separate from optional ones
    pass

# Or use a dictionary/class for related parameters
def create_user(user_data):
    # Access fields with user_data['name'], etc.
    pass

11.11.2 Side Effects Without Documentation

# Surprising side effect
def calculate_stats(data):
    """Calculate mean and median of data."""
    data.sort()  # Side effect: modifies input list!
    mean = sum(data) / len(data)
    median = data[len(data) // 2]
    return mean, median

# Better: document the side effect
def calculate_stats(data):
    """
    Calculate statistics from data.

    Note: This function sorts the input list in-place.
    """
    data.sort()
    # Rest of function...

# Best: avoid the side effect
def calculate_stats(data):
    """Calculate statistics from data (non-modifying)."""
    sorted_data = sorted(data)  # Creates a new sorted list
    # Use sorted_data instead of modifying input

11.11.3 Not Handling Edge Cases

# Missing edge case handling
def calculate_average(numbers):
    return sum(numbers) / len(numbers)  # Crashes on empty list

# Better approach
def calculate_average(numbers):
    if not numbers:
        return 0  # Or raise an exception, or return None
    return sum(numbers) / len(numbers)

11.11.4 Function Naming Confusion

# Confusing name - doesn't match behavior
def check_email(email):
    """Validates an email and returns a boolean."""
    return "@" in email and "." in email

# Better name
def is_valid_email(email):
    """Validates an email and returns a boolean."""
    return "@" in email and "." in email

# Or
def validate_email(email):
    """Validates an email and returns errors or None."""
    if "@" not in email:
        return "Email must contain @ symbol"
    if "." not in email:
        return "Email must contain a domain"
    return None  # No errors

11.11.5 Inconsistent Return Types

# Inconsistent - sometimes returns bool, sometimes string
def process_payment(amount):
    if amount <= 0:
        return "Invalid amount"  # String

    if process_successful:
        return True  # Boolean
    else:
        return "Payment failed"  # String

# Better approach - consistent return type
def process_payment(amount):
    """Process payment and return result with message."""
    if amount <= 0:
        return False, "Invalid amount"

    if process_successful:
        return True, "Payment successful"
    else:
        return False, "Payment failed"

11.11.6 Doing Too Much in One Function

# Function trying to do too much
def process_order(order_id):
    # Validate the order
    # Calculate the total
    # Process payment
    # Update inventory
    # Send confirmation email
    # Update order status
    pass

# Better: separate functions for each responsibility
def validate_order(order_id):
    # Validation logic
    pass

def calculate_order_total(order_items):
    # Calculation logic
    pass

def process_payment(amount, payment_method):
    # Payment processing
    pass

# Etc.

11.12 10. Real-World Function Examples

Let’s look at some realistic examples of function design in different contexts:

11.12.1 Data Processing Function

def clean_data(data, columns=None, drop_duplicates=True, fill_missing=None):
    """
    Clean a pandas DataFrame by handling specified issues.

    Args:
        data (pandas.DataFrame): The DataFrame to clean
        columns (list, optional): Specific columns to clean. Defaults to all columns.
        drop_duplicates (bool, optional): Whether to remove duplicate rows. Defaults to True.
        fill_missing (dict, optional): How to fill missing values {column: value}.
                                       Defaults to None (don't fill).

    Returns:
        pandas.DataFrame: The cleaned DataFrame
        dict: Summary of changes made
    """
    import pandas as pd

    # Validate input
    if not isinstance(data, pd.DataFrame):
        raise TypeError("data must be a pandas DataFrame")

    # Work on a copy to avoid modifying the original
    df = data.copy()

    # Track changes for summary
    changes = {
        "rows_before": len(df),
        "duplicates_removed": 0,
        "missing_values_filled": 0
    }

    # Use only specified columns if provided
    working_columns = columns if columns is not None else df.columns

    # Handle duplicates if requested
    if drop_duplicates:
        before_rows = len(df)
        df = df.drop_duplicates(subset=working_columns)
        changes["duplicates_removed"] = before_rows - len(df)

    # Fill missing values if specified
    if fill_missing:
        for col, value in fill_missing.items():
            if col in df.columns:
                missing_count = df[col].isna().sum()
                df[col] = df[col].fillna(value)
                changes["missing_values_filled"] += missing_count

    changes["rows_after"] = len(df)

    return df, changes

# Example usage:
# df, summary = clean_data(customer_data,
#                          columns=["name", "email"],
#                          fill_missing={"age": 0, "income": 0})

11.12.2 Web Application Function

def authenticate_user(username, password, max_attempts=3, lockout_minutes=30):
    """
    Authenticate a user against the database.

    Args:
        username (str): The username to authenticate
        password (str): The password to verify
        max_attempts (int, optional): Maximum failed attempts before lockout. Defaults to 3.
        lockout_minutes (int, optional): Minutes to lock account after max_attempts. Defaults to 30.

    Returns:
        dict: Authentication result with format:
              {"success": bool, "user_id": int or None, "message": str}

    Note:
        This function updates the database attempts counter and lockout status.
    """
    import datetime

    # Validate inputs
    if not username or not password:
        return {"success": False, "user_id": None, "message": "Username and password required"}

    # Check if user exists
    user = get_user_by_username(username)
    if not user:
        # Don't reveal that the username doesn't exist (security best practice)
        return {"success": False, "user_id": None, "message": "Invalid credentials"}

    # Check for account lockout
    if user.locked_until and user.locked_until > datetime.datetime.now():
        remaining_minutes = (user.locked_until - datetime.datetime.now()).seconds // 60
        return {
            "success": False,
            "user_id": None,
            "message": f"Account temporarily locked. Try again in {remaining_minutes} minutes."
        }

    # Reset lockout if it has expired
    if user.locked_until and user.locked_until <= datetime.datetime.now():
        user.failed_attempts = 0
        user.locked_until = None
        update_user(user)

    # Verify password
    if verify_password(password, user.password_hash):
        # Success: reset failed attempts and return success
        user.failed_attempts = 0
        update_user(user)
        return {"success": True, "user_id": user.id, "message": "Login successful"}
    else:
        # Failed attempt: increment counter
        user.failed_attempts += 1

        # Check if should lock account
        if user.failed_attempts >= max_attempts:
            lockout_time = datetime.datetime.now() + datetime.timedelta(minutes=lockout_minutes)
            user.locked_until = lockout_time
            message = f"Too many failed attempts. Account locked for {lockout_minutes} minutes."
        else:
            attempts_left = max_attempts - user.failed_attempts
            message = f"Invalid credentials. {attempts_left} attempts remaining."

        update_user(user)
        return {"success": False, "user_id": None, "message": message}

# Note: This assumes helper functions get_user_by_username(),
# verify_password(), and update_user() exist elsewhere

11.12.3 Game Development Function

def calculate_damage(attacker, defender, attack_type="normal"):
    """
    Calculate damage for an attack in a game.

    Args:
        attacker (dict): Attacker stats including 'strength', 'level', 'weapon_power'
        defender (dict): Defender stats including 'defense', 'level', 'resistance'
        attack_type (str, optional): Type of attack - "normal", "critical", or "special".
                                    Defaults to "normal".

    Returns:
        dict: Damage calculation results including:
              - 'damage': Final damage amount (int)
              - 'critical': Whether it was a critical hit (bool)
              - 'hit': Whether the attack landed (bool)
              - 'message': Description of the attack result (str)
    """
    import random

    # Base damage calculation
    base_damage = (attacker.get('strength', 0) + attacker.get('weapon_power', 0)) * 1.5

    # Accuracy check (chance to miss)
    accuracy = min(95, 80 + (attacker.get('level', 1) - defender.get('level', 1)) * 2)
    hit_roll = random.randint(1, 100)

    # Check if attack hits
    if hit_roll > accuracy:
        return {
            'damage': 0,
            'critical': False,
            'hit': False,
            'message': "The attack missed!"
        }

    # Critical hit chance
    critical_chance = min(25, 5 + attacker.get('level', 1) // 2)
    is_critical = random.randint(1, 100) <= critical_chance

    # Damage modifiers
    damage_multiplier = 1.0

    if is_critical:
        damage_multiplier *= 2.0

    if attack_type == "critical":
        damage_multiplier *= 1.5
    elif attack_type == "special":
        damage_multiplier *= 1.75

    # Apply defender's defense
    defense_reduction = defender.get('defense', 0) * 0.5
    damage = max(1, int((base_damage - defense_reduction) * damage_multiplier))

    # Apply damage resistance for special attacks
    if attack_type == "special":
        resistance = defender.get('resistance', 0) / 100
        damage = int(damage * (1 - min(0.75, resistance)))

    # Create result message
    if is_critical:
        message = "Critical hit! "
    else:
        message = ""

    if attack_type == "special":
        message += f"Special attack deals {damage} damage!"
    else:
        message += f"The attack deals {damage} damage."

    return {
        'damage': damage,
        'critical': is_critical,
        'hit': True,
        'message': message
    }

These examples demonstrate how functions in real-world applications handle multiple parameters, process complex logic, implement validation, and return structured results. Notice how each function:

  1. Has clear documentation
  2. Validates inputs
  3. Has sensible defaults for optional parameters
  4. Returns structured data with multiple pieces of information
  5. Maintains a single responsibility despite complex internal logic

11.13 11. Self-Assessment Quiz

Test your understanding of Python function creation:

  1. What is the correct syntax for defining a function that takes a parameter?
    1. function my_func(param):
    2. def my_func[param]:
    3. def my_func(param):
    4. new my_func(param):
  2. What happens if a function doesn’t include a return statement?
    1. The function returns the value of the last expression
    2. The function returns False
    3. The function returns None
    4. The function raises an error
  3. If a function has a parameter with a default value, where must it be placed?
    1. It must be the first parameter
    2. It must be after all parameters without default values
    3. It can be placed anywhere in the parameter list
    4. Default values are not allowed in function parameters
  4. What does the term “function scope” refer to?
    1. The range of values a function can return
    2. The visibility and lifetime of variables within a function
    3. The number of parameters a function can accept
    4. The performance characteristics of a function
  5. Which of the following is the best practice for function design?
    1. Creating functions with as many features as possible
    2. Using global variables for communication between functions
    3. Having functions perform a single, well-defined task
    4. Minimizing the number of functions in your program
  6. What is a “docstring” in a Python function?
    1. A special comment that becomes part of the function’s help documentation
    2. A mandatory error message for when the function fails
    3. A type of return value
    4. A system for categorizing functions
  7. When is it appropriate to use default parameter values?
    1. Never, they make functions confusing
    2. For parameters that are commonly passed the same value
    3. Only for numeric parameters
    4. Only when the function has exactly one parameter
  8. What happens to local variables when a function finishes execution?
    1. They remain in memory permanently
    2. They are destroyed and their memory is reclaimed
    3. They become global variables
    4. They are saved to disk

Answers: 1. c) def my_func(param): - This is the correct Python syntax for function definition 2. c) The function returns None - Python implicitly returns None if no return statement is provided 3. b) It must be after all parameters without default values - Python requires default parameters to come after non-default ones 4. b) The visibility and lifetime of variables within a function - Scope determines where variables can be accessed 5. c) Having functions perform a single, well-defined task - The single responsibility principle 6. a) A special comment that becomes part of the function’s help documentation - Docstrings document function purpose and usage 7. b) For parameters that are commonly passed the same value - Default values reduce repetition for common cases 8. b) They are destroyed and their memory is reclaimed - Local variables only exist during function execution

11.14 12. Try It Yourself: Function Design Exercises

Practice your function design skills with these exercises:

11.14.1 Exercise 1: Basic Function Creation

Create a function called temperature_converter that: - Takes a temperature value and conversion direction (‘C_to_F’ or ‘F_to_C’) - Performs the appropriate conversion - Returns the converted temperature with appropriate unit label - Validates that inputs are numeric and the direction is valid - Includes a proper docstring

11.14.2 Exercise 2: Text Analysis Functions

Create a set of text analysis functions: 1. count_words(text) - Counts the number of words in a text 2. count_characters(text, include_spaces=False) - Counts characters 3. find_most_common_word(text) - Finds the most frequently used word 4. text_statistics(text) - Returns a dictionary with all the above statistics

Ensure each function has proper validation, docstrings, and error handling.

11.14.3 Exercise 3: Chatbot Extension

Extend the chatbot from the Project Corner with these new functions: 1. get_time_based_greeting() - Returns a greeting based on the time of day 2. remember_topic(topic) - Stores a topic the user mentioned 3. recall_topics() - Returns previously discussed topics 4. generate_farewell(user_name) - Creates a personalized goodbye message Integrate these functions into the chatbot’s main loop.

11.14.4 Exercise 4: Game Score Calculator

Create a function that calculates a game score: - Takes player actions (hits, misses, bonuses) as parameters - Calculates a score based on a formula you design - Includes optional difficulty multiplier parameter - Validates all inputs - Returns both the score and performance category (e.g., “Beginner”, “Expert”)

11.14.5 Exercise 5: Function Library

Create a small library of related functions for managing a to-do list: 1. add_task(task_list, task_description, due_date=None, priority=None) 2. remove_task(task_list, task_index) 3. mark_complete(task_list, task_index) 4. get_pending_tasks(task_list) 5. get_task_summary(task_list) Ensure the functions work together cohesively and follow good design practices.

11.15 13. Cross-References

  • Previous Chapter: Functions — Learn how to use built-in and library functions
  • Next Chapter: Making Decisions — Control program flow with if statements
  • Related Topic: Modules and Packages — Organize your functions into reusable modules
  • Project Connection: Testing — Learn how to verify your functions work correctly
  • Advanced Application: Object-Oriented Programming — Combine functions and data into classes

11.16 14. Summary

In this chapter, you’ve learned the essential skills for creating your own custom functions in Python:

  • Using the def keyword to define functions
  • Creating flexible functions with parameters and default values
  • Returning results from functions with the return statement
  • Understanding variable scope and lifetime within functions
  • Applying function design best practices
  • Structuring your chatbot with modular, well-designed functions

Functions transform how you approach programming problems, allowing you to: - Break complex problems into manageable pieces - Create reusable solutions that eliminate redundancy - Make your code more readable and maintainable - Collaborate more effectively by defining clear interfaces

For our chatbot project, you’ve implemented a modular structure that separates concerns into individual functions. This approach makes your code easier to understand, debug, and extend. As we progress through the book, you’ll continue to refine these functions and add new capabilities.

Custom functions are one of the most powerful tools in programming. They allow you to create your own abstractions and define your own vocabulary for solving problems. By mastering function creation, you’ve taken a major step toward thinking like a programmer and building more sophisticated applications.

In the next chapter, we’ll explore how to make decisions in your code using conditional statements, which will further enhance your chatbot’s ability to respond intelligently to different situations.