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 functionfunction_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
# Output: Hello, world! greet()
Every time you call the function, its code executes:
# Hello, world!
greet() # Hello, world!
greet() # Hello, world! greet()
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 justtotal
) - 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
"Alice") # Output: Hello, Alice!
greet("Bob") # Output: Hello, Bob! greet(
11.5.1 Required Parameters
In the example above, name
is a required parameter. If you don’t provide it, Python raises an error:
# Error: greet() missing 1 required positional argument: 'name' greet()
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
"Charlie", 25, "San Francisco") display_info(
When calling functions with multiple parameters, you must provide them in the correct order, or use named arguments:
# Positional arguments (order matters)
"Charlie", 25, "San Francisco")
display_info(
# Named arguments (order doesn't matter)
="San Francisco", name="Charlie", age=25)
display_info(location
# Mix of positional and named (positional must come first)
"Charlie", location="San Francisco", age=25) display_info(
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
"Diana") # Output: Hello, Diana!
greet(
# Overriding the default
"Edward", "Good morning") # Output: Good morning, Edward! greet(
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
"""
= price * quantity
subtotal = subtotal * tax_rate
tax 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
= add(5, 3)
sum_result print(sum_result) # Output: 8
# Using the return value directly in an expression
= add(5, 3) + add(2, 1)
total 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."""
= 800
width = 600
height return width, height # Returns a tuple (800, 600)
# Unpacking the returned tuple
= get_dimensions()
screen_width, screen_height 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
= greet("Frank")
result 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
= "I'm a local variable"
message print(message)
# Output: I'm a local variable
show_local() # print(message) # Error: name 'message' is not defined
Parameters are also local variables:
def double(number): # 'number' is a local variable
return number * 2
= double(5)
result # 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
= 0
counter
def increment():
"""Try to increment the counter."""
# This creates a new local variable, not modifying the global one
= counter + 1 # UnboundLocalError
counter 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 + 1
counter 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
= 0
counter = increment(counter) # counter becomes 1 counter
11.7.2 Variable Lifetime
Local variables are created when the function is called and destroyed when it returns:
def process_data():
= 100 # Created when function starts
temp print(f"Processing at {temp}")
# 'temp' is destroyed when function ends
process_data()# Creates a fresh 'temp' variable process_data()
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}")
f"{name}@example.com")
validate_email(
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
= 0
total def add_to_total(value):
global total
+= value
total
# Better: pure function
def add(a, b):
return a + b
= add(total, value) # Explicit about what's changing total
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.lower()
user_input
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."""
= "PyBot"
bot_name print(f"Hello! I'm {bot_name}. Type 'bye' to exit.")
= input("What's your name? ")
user_name print(f"Nice to meet you, {user_name}!")
while True:
= input(f"{user_name}> ")
user_input if user_input.lower() == "bye":
print(f"{bot_name}> Goodbye, {user_name}!")
break
= get_response(user_input, user_name)
response 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:
= input("What's your name? ").strip()
name 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.lower()
user_input
# 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."""
= "PyBot"
bot_name
display_welcome(bot_name)= get_user_name()
user_name print(f"\n{bot_name}> Nice to meet you, {user_name}!\n")
while True:
= get_user_input(user_name)
user_input
# Check for exit command
if user_input.lower() == "bye":
f"Goodbye, {user_name}!")
display_response(bot_name, break
= get_response(user_input, user_name)
response
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:
- Each function has a single responsibility - input, output, logic, etc.
- Functions are composable - the main function calls smaller functions
- Error handling is built in - input validation in
get_user_name()
- Code is self-documenting - function names clearly describe their purpose
- 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
= datetime.now().year
current_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
= user_input.split()
words for word in words:
if word.isdigit() and 1900 <= int(word) <= 2020:
= calculate_age(int(word))
age 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
= handle_age_question(user_input)
age_response 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}!")
"Alice") # Output: Hello, Alice! greet(
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.lower()
message = message.split()
words
# 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
= custom_keywords if custom_keywords else default_keywords
keywords
# Count sentiment matches
= sum(1 for word in words if word in keywords['positive'])
positive_matches = sum(1 for word in words if word in keywords['negative'])
negative_matches = positive_matches + negative_matches
total_sentiment_words
# Calculate sentiment and confidence
if total_sentiment_words == 0:
return 'neutral', 0.5 # No sentiment words found
if positive_matches > negative_matches:
= 'positive'
sentiment = positive_matches / total_sentiment_words
confidence elif negative_matches > positive_matches:
= 'negative'
sentiment = negative_matches / total_sentiment_words
confidence else:
= 'neutral'
sentiment = 0.5
confidence
# Adjust confidence to be between 0.5 and 1.0 (0.5 = neutral)
= 0.5 + (confidence * 0.5)
confidence
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:
= analyze_sentiment(message)
sentiment, confidence print(f"Message: '{message}'")
print(f"Sentiment: {sentiment}, Confidence: {confidence:.2f}\n")
Design decisions explained:
- 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
- Primary parameter (
- Robust validation:
- Type checking for parameters
- Empty message validation
- Explicit error messages for troubleshooting
- 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
- Complete documentation:
- Clear description of function purpose
- Detailed parameter documentation
- Return value format explanation
- Documentation of potential errors
- 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:
Specify the exact purpose of the function you need
Describe inputs and outputs clearly
Mention any special requirements like error handling or validation
Ask about design considerations specific to your use case
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."""
# Side effect: modifies input list!
data.sort() = sum(data) / len(data)
mean = data[len(data) // 2]
median 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) # Creates a new sorted list
sorted_data # 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
= data.copy()
df
# Track changes for summary
= {
changes "rows_before": len(df),
"duplicates_removed": 0,
"missing_values_filled": 0
}
# Use only specified columns if provided
= columns if columns is not None else df.columns
working_columns
# Handle duplicates if requested
if drop_duplicates:
= len(df)
before_rows = df.drop_duplicates(subset=working_columns)
df "duplicates_removed"] = before_rows - len(df)
changes[
# Fill missing values if specified
if fill_missing:
for col, value in fill_missing.items():
if col in df.columns:
= df[col].isna().sum()
missing_count = df[col].fillna(value)
df[col] "missing_values_filled"] += missing_count
changes[
"rows_after"] = len(df)
changes[
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
= get_user_by_username(username)
user 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():
= (user.locked_until - datetime.datetime.now()).seconds // 60
remaining_minutes 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():
= 0
user.failed_attempts = None
user.locked_until
update_user(user)
# Verify password
if verify_password(password, user.password_hash):
# Success: reset failed attempts and return success
= 0
user.failed_attempts
update_user(user)return {"success": True, "user_id": user.id, "message": "Login successful"}
else:
# Failed attempt: increment counter
+= 1
user.failed_attempts
# Check if should lock account
if user.failed_attempts >= max_attempts:
= datetime.datetime.now() + datetime.timedelta(minutes=lockout_minutes)
lockout_time = lockout_time
user.locked_until = f"Too many failed attempts. Account locked for {lockout_minutes} minutes."
message else:
= max_attempts - user.failed_attempts
attempts_left = f"Invalid credentials. {attempts_left} attempts remaining."
message
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
= (attacker.get('strength', 0) + attacker.get('weapon_power', 0)) * 1.5
base_damage
# Accuracy check (chance to miss)
= min(95, 80 + (attacker.get('level', 1) - defender.get('level', 1)) * 2)
accuracy = random.randint(1, 100)
hit_roll
# Check if attack hits
if hit_roll > accuracy:
return {
'damage': 0,
'critical': False,
'hit': False,
'message': "The attack missed!"
}
# Critical hit chance
= min(25, 5 + attacker.get('level', 1) // 2)
critical_chance = random.randint(1, 100) <= critical_chance
is_critical
# Damage modifiers
= 1.0
damage_multiplier
if is_critical:
*= 2.0
damage_multiplier
if attack_type == "critical":
*= 1.5
damage_multiplier elif attack_type == "special":
*= 1.75
damage_multiplier
# Apply defender's defense
= defender.get('defense', 0) * 0.5
defense_reduction = max(1, int((base_damage - defense_reduction) * damage_multiplier))
damage
# Apply damage resistance for special attacks
if attack_type == "special":
= defender.get('resistance', 0) / 100
resistance = int(damage * (1 - min(0.75, resistance)))
damage
# Create result message
if is_critical:
= "Critical hit! "
message else:
= ""
message
if attack_type == "special":
+= f"Special attack deals {damage} damage!"
message else:
+= f"The attack deals {damage} damage."
message
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:
- Has clear documentation
- Validates inputs
- Has sensible defaults for optional parameters
- Returns structured data with multiple pieces of information
- Maintains a single responsibility despite complex internal logic
11.13 11. Self-Assessment Quiz
Test your understanding of Python function creation:
- What is the correct syntax for defining a function that takes a parameter?
function my_func(param):
def my_func[param]:
def my_func(param):
new my_func(param):
- What happens if a function doesn’t include a
return
statement?- The function returns the value of the last expression
- The function returns
False
- The function returns
None
- The function raises an error
- If a function has a parameter with a default value, where must it be placed?
- It must be the first parameter
- It must be after all parameters without default values
- It can be placed anywhere in the parameter list
- Default values are not allowed in function parameters
- What does the term “function scope” refer to?
- The range of values a function can return
- The visibility and lifetime of variables within a function
- The number of parameters a function can accept
- The performance characteristics of a function
- Which of the following is the best practice for function design?
- Creating functions with as many features as possible
- Using global variables for communication between functions
- Having functions perform a single, well-defined task
- Minimizing the number of functions in your program
- What is a “docstring” in a Python function?
- A special comment that becomes part of the function’s help documentation
- A mandatory error message for when the function fails
- A type of return value
- A system for categorizing functions
- When is it appropriate to use default parameter values?
- Never, they make functions confusing
- For parameters that are commonly passed the same value
- Only for numeric parameters
- Only when the function has exactly one parameter
- What happens to local variables when a function finishes execution?
- They remain in memory permanently
- They are destroyed and their memory is reclaimed
- They become global variables
- 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.