Skip to content
Home » Blog » How to Create and Use Functions in Python

How to Create and Use Functions in Python

How to Create and Use Functions in Python: Complete Guide from Basic to Advanced

How to Create and Use Functions in Python: Complete Guide from Basic to Advanced

What You Will Learn

Why Functions Matter in Python Programming

Just Consider Python functions as mini-programs inside your main program. Just like you have different tools in a toolbox, functions are coding tools that help you organize and reuse your code efficiently.

Before we start building Python functions, let me explain why they are crucial for programming success. When you write code without functions, you repeat the same instructions multiple times. This makes your Python code long, hard to read, and difficult to debug when problems occur.

Python functions solve this problem by letting you write code once and use it many times throughout your program. This concept is fundamental to defining and calling functions in Python, which forms the backbone of efficient programming and software development.

Real-World Programming Example

Imagine you work at a coffee shop. Instead of explaining how to make coffee to every customer, you have a recipe (function) that any barista can follow. The recipe stays the same, but you can make different types of coffee by changing the ingredients (function parameters). Python functions work exactly the same way in programming.

In this comprehensive Python functions tutorial, you will learn everything about creating, using, and mastering Python functions step by step. We start with simple function examples that beginners can understand, then move to advanced Python function topics that professional developers use in real-world projects every day.

Python functions help you write cleaner code, reduce programming errors, make debugging easier, and improve code reusability. Whether you’re building web applications, data analysis scripts, or automation tools, understanding Python functions is essential for becoming a skilled Python programmer.

Python Function Visualizations - Complete overview of function concepts

Click image to view full size

Creating Your First Python Function

Let me show you how to create your first Python function step by step. In Python programming, we use the keyword def to define a function. This keyword tells Python “I want to create a new function with specific instructions.”

The Basic Python Function Structure

Every Python function follows this exact pattern. This is the foundation of Python function syntax:

Basic Function Structure
def function_name():
    # Your code goes here
    print("This is inside the function")

# Call the function
function_name()
Output:
This is inside the function

Let me break down each part of Python function syntax for you:

  • def – This Python keyword tells the interpreter you want to create a function
  • function_name – This is the name you give your Python function for calling it later
  • () – These parentheses hold function parameters (we will learn about Python function parameters soon)
  • : – This colon starts the Python function body
  • The indented code – This is the actual Python code that the function executes

Your First Real Python Function Example

Let’s create a simple Python function that greets people. This example shows how Python functions work in practice:

Simple Greeting Function
def say_hello():
    print("Hello there!")
    print("Welcome to Python functions!")
    print("Hope you have a great day!")

# Now let's use our function
say_hello()
print("Function finished running")

# We can call it again
say_hello()
Output:
Hello there! Welcome to Python functions! Hope you have a great day! Function finished running Hello there! Welcome to Python functions! Hope you have a great day!

Notice how we called the Python function twice, and it executed the same code both times. This demonstrates the power of Python functions – write code once, use it many times throughout your program!

Important Rule

In Python, you must define a function before you can use it. If you try to call a function before defining it, Python will give you an error saying the function doesn’t exist.

Function Names Matter

When naming your functions, follow these simple rules:

  • Use lowercase letters
  • Separate words with underscores
  • Make the name describe what the function does
  • Don’t use Python keywords like print, if, for
Good and Bad Function Names
# Good function names
def calculate_total():
    pass

def send_email():
    pass

def check_password():
    pass

# Bad function names (don't do this)
def a():  # Too short, unclear
    pass

def doStuff():  # CamelCase not recommended in Python
    pass

def print():  # This shadows Python's built-in print function
    pass
Python Function Parameter Types - Visual guide to function parameters

Click image to view full size

Try It Yourself

Create a function called show_menu that prints out a simple restaurant menu. Include at least 3 food items with prices. Then call your function to see the menu display.

Working with Function Parameters

Parameters make functions much more powerful. They let you pass information into your function so it can work with different data each time you call it.

Understanding how to use function parameters and return values in Python is crucial for writing flexible and reusable code.

Single Parameter Functions

Let’s start with a function that takes one piece of information:

Function with One Parameter
def greet_person(name):
    print(f"Hello, {name}!")
    print(f"Nice to meet you, {name}!")

# Call the function with different names
greet_person("Alice")
greet_person("Bob")
greet_person("Charlie")
Output:
Hello, Alice! Nice to meet you, Alice! Hello, Bob! Nice to meet you, Bob! Hello, Charlie! Nice to meet you, Charlie!

In this example, name is a parameter. When we call the function, we pass an argument (like “Alice”) that becomes the value of the parameter inside the function.

Parameter vs Argument

People often confuse these terms:

  • Parameter: The variable name in the function definition (like name)
  • Argument: The actual value you pass when calling the function (like “Alice”)

Multiple Parameters

Functions can take multiple parameters. Just separate them with commas:

Function with Multiple Parameters
def introduce_person(first_name, last_name, age):
    print(f"Name: {first_name} {last_name}")
    print(f"Age: {age} years old")
    print("-" * 25)

# Call with different people
introduce_person("John", "Smith", 25)
introduce_person("Mary", "Johnson", 30)
introduce_person("David", "Wilson", 22)
Output:
Name: John Smith Age: 25 years old ————————- Name: Mary Johnson Age: 30 years old ————————- Name: David Wilson Age: 22 years old ————————-

Notice how the order of arguments matters. The first argument goes to the first parameter, the second argument goes to the second parameter, and so on.

Default Parameters

Sometimes you want a parameter to have a default value. This means if someone doesn’t provide that argument, the function will use the default instead:

Function with Default Parameters
def create_profile(name, age, city="Unknown"):
    print(f"Profile Created:")
    print(f"Name: {name}")
    print(f"Age: {age}")
    print(f"City: {city}")
    print("-" * 20)

# Call with all arguments
create_profile("Alice", 28, "New York")

# Call without city (uses default)
create_profile("Bob", 35)

# Call with city specified
create_profile("Carol", 42, "Los Angeles")
Output:
Profile Created: Name: Alice Age: 28 City: New York ——————– Profile Created: Name: Bob Age: 35 City: Unknown ——————– Profile Created: Name: Carol Age: 42 City: Los Angeles ——————–

Default parameters must come after regular parameters. You can’t put a regular parameter after a default parameter.

Keyword Arguments

You can also call functions by specifying the parameter names. This makes your code clearer and lets you pass arguments in any order:

Using Keyword Arguments
def order_pizza(size, toppings, crust="thin"):
    print(f"Pizza Order:")
    print(f"Size: {size}")
    print(f"Toppings: {toppings}")
    print(f"Crust: {crust}")
    print("-" * 15)

# Regular way (order matters)
order_pizza("large", "pepperoni", "thick")

# Using keyword arguments (order doesn't matter)
order_pizza(toppings="mushrooms", size="medium", crust="stuffed")

# Mix of both
order_pizza("small", toppings="cheese")
Output:
Pizza Order: Size: large Toppings: pepperoni Crust: thick ————— Pizza Order: Size: medium Toppings: mushrooms Crust: stuffed ————— Pizza Order: Size: small Toppings: cheese Crust: thin —————
Variable Scope in Python Functions - Understanding local and global variables

Click image to view full size

Practice Exercise

Create a function called calculate_rectangle_area that takes length and width as parameters. Give width a default value of 1. The function should print the area calculation and result.

Understanding Return Values

So far, our functions have only printed things. But functions can also give back (return) values that you can use in other parts of your program.

What Are Return Values?

A return value is like the answer a function gives you after it finishes its work. It is like asking a calculator to add 2 + 3. The calculator does the work and gives you back the answer: 5.

Function That Returns a Value
def add_numbers(num1, num2):
    result = num1 + num2
    return result

# Call the function and store the result
answer = add_numbers(5, 3)
print(f"The answer is: {answer}")

# Use the result directly
print(f"Double the answer is: {add_numbers(5, 3) * 2}")

# You can use the returned value in other calculations
total = add_numbers(10, 15) + add_numbers(2, 8)
print(f"Total of both additions: {total}")
Output:
The answer is: 8 Double the answer is: 16 Total of both additions: 35

The return statement does two things:

  1. It gives back a value to whoever called the function
  2. It immediately exits the function (no code after return will run)

Functions Without Return Statements

What happens if a function doesn’t have a return statement? Python automatically returns None:

Function Without Return
def just_print_hello():
    print("Hello!")

# This function returns None
result = just_print_hello()
print(f"The function returned: {result}")
print(f"Type of result: {type(result)}")
Output:
Hello! The function returned: None Type of result: <class ‘NoneType’>

Returning Multiple Values

Python functions can return multiple values at once. This is very useful when you need to give back more than one piece of information:

Returning Multiple Values
def calculate_rectangle(length, width):
    area = length * width
    perimeter = 2 * (length + width)
    return area, perimeter

# Get both values
rect_area, rect_perimeter = calculate_rectangle(5, 3)
print(f"Area: {rect_area}")
print(f"Perimeter: {rect_perimeter}")

# Or get them as a tuple
measurements = calculate_rectangle(4, 6)
print(f"All measurements: {measurements}")
print(f"Area from tuple: {measurements[0]}")
print(f"Perimeter from tuple: {measurements[1]}")
Output:
Area: 15 Perimeter: 16 All measurements: (24, 20) Area from tuple: 24 Perimeter from tuple: 20

Early Returns

Sometimes you want to exit a function early based on certain conditions:

Early Return Example
def divide_numbers(a, b):
    if b == 0:
        print("Error: Cannot divide by zero!")
        return None
    
    result = a / b
    print(f"{a} divided by {b} equals {result}")
    return result

# Test with valid numbers
answer1 = divide_numbers(10, 2)
print(f"Result stored: {answer1}")

# Test with division by zero
answer2 = divide_numbers(10, 0)
print(f"Result stored: {answer2}")

# Test with more valid numbers
answer3 = divide_numbers(15, 3)
print(f"Result stored: {answer3}")
Output:
10 divided by 2 equals 5.0 Result stored: 5.0 Error: Cannot divide by zero! Result stored: None 15 divided by 3 equals 5.0 Result stored: 5.0

Important Note

Once a function hits a return statement, it immediately stops running. Any code after the return statement will not execute. This is useful for error handling and early exits.

Different types of return values in Python functions - Complete guide

Click image to view full size

Practice Challenge

Create a function called convert_temperature that takes a temperature in Celsius and returns both Fahrenheit and Kelvin. Use the formulas: F = (C × 9/5) + 32 and K = C + 273.15

Variable Scope: Local vs Global

Understanding variable scope is crucial for writing functions that work correctly. Scope determines where in your program you can use a variable.

Local Variables

Variables created inside a function are called local variables. They only exist while the function is running and can only be used inside that function:

Local Variables Example
def calculate_discount():
    # These variables only exist inside this function
    original_price = 100
    discount_rate = 0.2
    discount_amount = original_price * discount_rate
    final_price = original_price - discount_amount
    
    print(f"Original price: ${original_price}")
    print(f"Discount: ${discount_amount}")
    print(f"Final price: ${final_price}")

# Call the function
calculate_discount()

# Try to use the local variable outside the function
# This will cause an error!
try:
    print(original_price)
except NameError as e:
    print(f"Error: {e}")
    print("original_price doesn't exist outside the function!")
Output:
Original price: $100 Discount: $20.0 Final price: $80.0 Error: name ‘original_price’ is not defined original_price doesn’t exist outside the function!

Global Variables

Variables created outside of functions are called global variables. They can be used anywhere in your program:

Global Variables Example
# This is a global variable
company_name = "TechCorp"
tax_rate = 0.08

def calculate_order_total(item_price, quantity):
    # We can read global variables inside functions
    subtotal = item_price * quantity
    tax_amount = subtotal * tax_rate
    total = subtotal + tax_amount
    
    print(f"Company: {company_name}")
    print(f"Subtotal: ${subtotal}")
    print(f"Tax: ${tax_amount:.2f}")
    print(f"Total: ${total:.2f}")
    return total

# Use the function
final_total = calculate_order_total(25.00, 3)

# We can also use global variables outside functions
print(f"\nThank you for shopping with {company_name}!")
print(f"Current tax rate: {tax_rate * 100}%")
Output:
Company: TechCorp Subtotal: $75.0 Tax: $6.00 Total: $81.00 Thank you for shopping with TechCorp! Current tax rate: 8.0%

The Global Keyword

If you want to modify a global variable inside a function, you need to use the global keyword:

Modifying Global Variables
# Global counter
visitor_count = 0

def welcome_visitor():
    global visitor_count  # Tell Python we want to modify the global variable
    visitor_count += 1    # Increase the counter
    print(f"Welcome! You are visitor number {visitor_count}")

def show_statistics():
    # We can read global variables without the global keyword
    print(f"Total visitors today: {visitor_count}")

# Simulate some visitors
welcome_visitor()
welcome_visitor()
show_statistics()
welcome_visitor()
show_statistics()
Output:
Welcome! You are visitor number 1 Welcome! You are visitor number 2 Total visitors today: 2 Welcome! You are visitor number 3 Total visitors today: 3

Local Variables Shadow Global Variables

If you create a local variable with the same name as a global variable, the local variable “shadows” the global one inside the function:

Variable Shadowing Example
# Global variable
message = "I am global"

def test_shadowing():
    # Local variable with same name as global
    message = "I am local"
    print(f"Inside function: {message}")

def test_global():
    # This function uses the global variable
    print(f"Inside function (global): {message}")

print(f"Before functions: {message}")
test_shadowing()
test_global()
print(f"After functions: {message}")
Output:
Before functions: I am global Inside function: I am local Inside function (global): I am global After functions: I am global

Best Practice

Try to avoid using global variables when possible. Instead, pass values as parameters and return results. This makes your functions easier to understand and test. Global variables should mainly be used for constants or configuration settings.

Understanding Check

Can you predict what this code will print before running it? Try to trace through the variable scopes.

Scope Challenge
x = 10

def outer_function():
    x = 20
    
    def inner_function():
        x = 30
        print(f"Inner: {x}")
    
    inner_function()
    print(f"Outer: {x}")

print(f"Global: {x}")
outer_function()
print(f"Global after: {x}")

Advanced Parameter Techniques

Now that you understand basic parameters, let’s explore more advanced ways to work with function arguments. These techniques are used by professional Python developers to create flexible and powerful functions.

Variable Number of Arguments (*args)

Sometimes you don’t know how many arguments someone will pass to your function. The *args parameter lets your function accept any number of arguments:

Using *args
def add_all_numbers(*args):
    print(f"Received {len(args)} numbers: {args}")
    total = 0
    for number in args:
        total += number
    return total

# Call with different numbers of arguments
result1 = add_all_numbers(1, 2, 3)
print(f"Sum of 1, 2, 3 is: {result1}")

result2 = add_all_numbers(10, 20, 30, 40, 50)
print(f"Sum of 10, 20, 30, 40, 50 is: {result2}")

result3 = add_all_numbers(100)
print(f"Sum of just 100 is: {result3}")

# Even call with no arguments
result4 = add_all_numbers()
print(f"Sum of no numbers is: {result4}")
Output:
Received 3 numbers: (1, 2, 3) Sum of 1, 2, 3 is: 6 Received 5 numbers: (10, 20, 30, 40, 50) Sum of 10, 20, 30, 40, 50 is: 150 Received 1 numbers: (100,) Sum of just 100 is: 100 Received 0 numbers: () Sum of no numbers is: 0

The *args collects all the arguments into a tuple. You can name it anything (like *numbers or *values), but *args is the standard convention.

Variable Number of Keyword Arguments (**kwargs)

Similar to *args, **kwargs lets your function accept any number of keyword arguments:

Using **kwargs
def create_user_profile(**kwargs):
    print("Creating user profile with:")
    for key, value in kwargs.items():
        print(f"  {key}: {value}")
    print("-" * 25)

# Call with different keyword arguments
create_user_profile(name="Alice", age=25, city="New York")

create_user_profile(
    name="Bob", 
    age=30, 
    email="bob@email.com", 
    phone="555-1234",
    occupation="Engineer"
)

create_user_profile(name="Charlie")
Output:
Creating user profile with: name: Alice age: 25 city: New York ————————- Creating user profile with: name: Bob age: 30 email: bob@email.com phone: 555-1234 occupation: Engineer ————————- Creating user profile with: name: Charlie ————————-

Combining Different Parameter Types

You can combine regular parameters, default parameters, *args, and **kwargs in the same function. However, they must be in this specific order:

Combined Parameter Types
def flexible_function(required_param, default_param="default", *args, **kwargs):
    print(f"Required parameter: {required_param}")
    print(f"Default parameter: {default_param}")
    print(f"Extra arguments: {args}")
    print(f"Keyword arguments: {kwargs}")
    print("-" * 30)

# Different ways to call this function
flexible_function("Must provide this")

flexible_function("Required", "Custom default")

flexible_function("Required", "Custom", 1, 2, 3)

flexible_function(
    "Required", 
    "Custom", 
    1, 2, 3, 
    name="Alice", 
    age=25
)
Output:
Required parameter: Must provide this Default parameter: default Extra arguments: () Keyword arguments: {} —————————— Required parameter: Required Default parameter: Custom default Extra arguments: () Keyword arguments: {} —————————— Required parameter: Required Default parameter: Custom Extra arguments: (1, 2, 3) Keyword arguments: {} —————————— Required parameter: Required Default parameter: Custom Extra arguments: (1, 2, 3) Keyword arguments: {‘name’: ‘Alice’, ‘age’: 25} ——————————

Unpacking Arguments

You can also use * and ** when calling functions to unpack lists, tuples, and dictionaries:

Unpacking Arguments
def calculate_rectangle_info(length, width, unit="cm"):
    area = length * width
    perimeter = 2 * (length + width)
    print(f"Rectangle: {length}{unit} x {width}{unit}")
    print(f"Area: {area} {unit}²")
    print(f"Perimeter: {perimeter} {unit}")
    print("-" * 20)

# Normal function call
calculate_rectangle_info(5, 3)

# Using a list with * to unpack
dimensions = [8, 6]
calculate_rectangle_info(*dimensions)

# Using a dictionary with ** to unpack
rectangle_data = {
    "length": 10,
    "width": 4,
    "unit": "inches"
}
calculate_rectangle_info(**rectangle_data)

# Combining unpacking
coordinates = [12, 9]
settings = {"unit": "meters"}
calculate_rectangle_info(*coordinates, **settings)
Output:
Rectangle: 5cm x 3cm Area: 15 cm² Perimeter: 16 cm ——————– Rectangle: 8cm x 6cm Area: 48 cm² Perimeter: 28 cm ——————– Rectangle: 10inches x 4inches Area: 40 inches² Perimeter: 28 inches ——————– Rectangle: 12meters x 9meters Area: 108 meters² Perimeter: 42 meters ——————–

When to Use Advanced Parameters

  • *args: When you need to process a varying number of similar items
  • **kwargs: When you want to accept optional configuration or metadata
  • Unpacking: When you have data in lists or dictionaries that match function parameters

Advanced Challenge

Create a function called build_sandwich that takes a required bread type, optional spread (default: “butter”), any number of fillings (*args), and any number of extras as keyword arguments (**kwargs). The function should print out the complete sandwich description.

Lambda Functions Explained

Lambda functions are a way to create small, simple functions in just one line. They’re perfect for situations where you need a quick function that you’ll only use once or twice.

What Are Lambda Functions?

A lambda function is like a mini-function. Instead of using def and giving it a name, you create it on the spot. It is like a mathematical formula:

Regular Function vs Lambda Function
# Regular function
def square_regular(x):
    return x * x

# Lambda function (does the same thing)
square_lambda = lambda x: x * x

# Both work the same way
print(f"Regular function: 5 squared = {square_regular(5)}")
print(f"Lambda function: 5 squared = {square_lambda(5)}")

# You can use lambda directly without assigning to a variable
result = (lambda x: x * x)(7)
print(f"Direct lambda use: 7 squared = {result}")
Output:
Regular function: 5 squared = 25 Lambda function: 5 squared = 25 Direct lambda use: 7 squared = 49

The lambda syntax is: lambda parameters: expression

  • lambda – The keyword that starts a lambda function
  • parameters – The inputs (like regular function parameters)
  • : – Separates parameters from the expression
  • expression – What the function returns (no return keyword needed)

Lambda Functions with Multiple Parameters

Lambda functions can take multiple parameters, just like regular functions:

Multiple Parameter Lambda Functions
# Lambda with two parameters
add = lambda x, y: x + y
print(f"3 + 7 = {add(3, 7)}")

# Lambda with three parameters
calculate_total = lambda price, tax_rate, tip_rate: price * (1 + tax_rate + tip_rate)
bill_total = calculate_total(50, 0.08, 0.15)
print(f"Total bill: ${bill_total:.2f}")

# Lambda with default parameter
greet = lambda name, greeting="Hello": f"{greeting}, {name}!"
print(greet("Alice"))
print(greet("Bob", "Hi there"))
Output:
3 + 7 = 10 Total bill: $61.50 Hello, Alice! Hi there, Bob!

Using Lambda Functions with Built-in Functions

Lambda functions are most useful when combined with built-in functions like map(), filter(), and sorted(). These functions work with Python data structures like lists and sets.

Using map() with Lambda

The map() function applies a function to every item in a list:

Lambda with map()
# Convert temperatures from Celsius to Fahrenheit
celsius_temps = [0, 20, 30, 37, 100]

# Using lambda with map
fahrenheit_temps = list(map(lambda c: (c * 9/5) + 32, celsius_temps))

print("Temperature conversion:")
for c, f in zip(celsius_temps, fahrenheit_temps):
    print(f"{c}°C = {f}°F")

print()

# Square all numbers in a list
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
print(f"Original numbers: {numbers}")
print(f"Squared numbers: {squared}")
Output:
Temperature conversion: 0°C = 32.0°F 20°C = 68.0°F 30°C = 86.0°F 37°C = 98.6°F 100°C = 212.0°F Original numbers: [1, 2, 3, 4, 5] Squared numbers: [1, 4, 9, 16, 25]

Using filter() with Lambda

The filter() function keeps only the items that meet a certain condition:

Lambda with filter()
# Filter even numbers
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))

print(f"All numbers: {numbers}")
print(f"Even numbers: {even_numbers}")

# Filter names that start with 'A'
names = ["Alice", "Bob", "Anna", "Charlie", "Andrew", "Diana"]
a_names = list(filter(lambda name: name.startswith('A'), names))

print(f"All names: {names}")
print(f"Names starting with 'A': {a_names}")

# Filter products by price
products = [
    {"name": "Laptop", "price": 999},
    {"name": "Mouse", "price": 25},
    {"name": "Keyboard", "price": 75},
    {"name": "Monitor", "price": 300}
]

affordable = list(filter(lambda product: product["price"] < 100, products))
print("\nAffordable products (under $100):")
for product in affordable:
    print(f"  {product['name']}: ${product['price']}")
Output:
All numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] Even numbers: [2, 4, 6, 8, 10] All names: ['Alice', 'Bob', 'Anna', 'Charlie', 'Andrew', 'Diana'] Names starting with 'A': ['Alice', 'Anna', 'Andrew'] Affordable products (under $100): Mouse: $25 Keyboard: $75

Using sorted() with Lambda

The sorted() function can use a lambda to determine how to sort items:

Lambda with sorted()
# Sort by string length
words = ["python", "java", "c", "javascript", "go", "rust"]
sorted_by_length = sorted(words, key=lambda word: len(word))

print(f"Original order: {words}")
print(f"Sorted by length: {sorted_by_length}")

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

sorted_by_grade = sorted(students, key=lambda student: student["grade"], reverse=True)
print("\nStudents sorted by grade (highest first):")
for student in sorted_by_grade:
    print(f"  {student['name']}: {student['grade']}")

# Sort coordinates by distance from origin
coordinates = [(3, 4), (1, 1), (5, 2), (2, 3)]
import math
sorted_by_distance = sorted(coordinates, key=lambda point: math.sqrt(point[0]**2 + point[1]**2))

print(f"\nCoordinates: {coordinates}")
print(f"Sorted by distance from origin: {sorted_by_distance}")
Output:
Original order: ['python', 'java', 'c', 'javascript', 'go', 'rust'] Sorted by length: ['c', 'go', 'java', 'rust', 'python', 'javascript'] Students sorted by grade (highest first): Diana: 96 Bob: 92 Alice: 85 Charlie: 78 Coordinates: [(3, 4), (1, 1), (5, 2), (2, 3)] Sorted by distance from origin: [(1, 1), (2, 3), (3, 4), (5, 2)]

When to Use Lambda Functions

Lambda vs Regular Functions

Use lambda when:

  • You need a simple, one-line function
  • You're using it with map(), filter(), sorted(), etc.
  • The function is used only once or twice

Use regular functions when:

  • The function is complex or has multiple lines
  • You need to reuse the function many times
  • You want to give the function a descriptive name

Lambda Practice

Try creating lambda functions for these tasks:

  1. Convert a list of prices from dollars to euros (multiply by 0.85)
  2. Filter a list of ages to keep only those 18 and older
  3. Sort a list of dictionaries by a specific key

Introduction to Decorators

Decorators are a powerful Python feature that lets you modify or extend the behavior of functions without changing their code. Just consider a decorator like gift wrapping - it adds something extra around your function.

Understanding Decorators

Before we create decorators, you need to understand that in Python, functions are objects. This means you can pass functions as arguments to other functions:

Functions as Objects
def say_hello():
    return "Hello!"

def say_goodbye():
    return "Goodbye!"

def call_function(func):
    print(f"Calling function: {func.__name__}")
    result = func()
    print(f"Function returned: {result}")
    return result

# Pass functions as arguments
call_function(say_hello)
print("-" * 20)
call_function(say_goodbye)
Output:
Calling function: say_hello Function returned: Hello! Hello! -------------------- Calling function: say_goodbye Function returned: Goodbye! Goodbye!

Creating Your First Decorator

A decorator is a function that takes another function and extends its behavior. Here's a simple example:

Simple Decorator
def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

def say_whee():
    print("Whee!")

# Apply the decorator manually
say_whee = my_decorator(say_whee)
say_whee()
Output:
Something is happening before the function is called. Whee! Something is happening after the function is called.

Using the @ Symbol

Python provides a cleaner way to apply decorators using the @ symbol:

Using @ for Decorators
def timer_decorator(func):
    import time
    def wrapper():
        start_time = time.time()
        print(f"Starting {func.__name__}...")
        
        func()  # Call the original function
        
        end_time = time.time()
        print(f"Finished {func.__name__} in {end_time - start_time:.4f} seconds")
    return wrapper

@timer_decorator
def slow_function():
    import time
    print("Doing some work...")
    time.sleep(1)  # Simulate slow work
    print("Work completed!")

# Call the decorated function
slow_function()
Output:
Starting slow_function... Doing some work... Work completed! Finished slow_function in 1.0041 seconds

Decorators with Arguments

To make decorators work with functions that have parameters, you need to use *args and **kwargs:

Decorator with Arguments
def log_function_call(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with:")
        print(f"  Arguments: {args}")
        print(f"  Keyword arguments: {kwargs}")
        
        result = func(*args, **kwargs)
        
        print(f"  Returned: {result}")
        return result
    return wrapper

@log_function_call
def add_numbers(a, b):
    return a + b

@log_function_call
def greet_person(name, greeting="Hello"):
    return f"{greeting}, {name}!"

# Test the decorated functions
result1 = add_numbers(5, 3)
print(f"Final result: {result1}")

print("-" * 30)

result2 = greet_person("Alice", greeting="Hi")
print(f"Final result: {result2}")
Output:
Calling add_numbers with: Arguments: (5, 3) Keyword arguments: {} Returned: 8 Final result: 8 ------------------------------ Calling greet_person with: Arguments: ('Alice',) Keyword arguments: {'greeting': 'Hi'} Returned: Hi, Alice! Final result: Hi, Alice!

Practical Decorator Examples

Here are some useful decorators you might use in real projects:

Practical Decorators
# Decorator to retry failed functions
def retry(max_attempts=3):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    print(f"Attempt {attempt} of {max_attempts}")
                    result = func(*args, **kwargs)
                    print(f"Success on attempt {attempt}!")
                    return result
                except Exception as e:
                    print(f"Attempt {attempt} failed: {e}")
                    if attempt == max_attempts:
                        print("All attempts failed!")
                        raise
            return None
        return wrapper
    return decorator

# Decorator to validate input
def validate_positive(func):
    def wrapper(*args, **kwargs):
        for arg in args:
            if isinstance(arg, (int, float)) and arg <= 0:
                raise ValueError(f"All arguments must be positive, got {arg}")
        result = func(*args, **kwargs)
        return result
    return wrapper

@retry(max_attempts=2)
def unreliable_function():
    import random
    if random.random() < 0.7:  # 70% chance of failure
        raise Exception("Random failure!")
    return "Success!"

@validate_positive
def calculate_area(length, width):
    return length * width

# Test the retry decorator
try:
    result = unreliable_function()
    print(f"Final result: {result}")
except:
    print("Function ultimately failed")

print("-" * 30)

# Test the validation decorator
try:
    area1 = calculate_area(5, 3)
    print(f"Area: {area1}")
    
    area2 = calculate_area(-2, 4)  # This will fail
    print(f"Area: {area2}")
except ValueError as e:
    print(f"Validation error: {e}")
Output:
Attempt 1 of 2 Random failure! Attempt 2 of 2 Success on attempt 2! Final result: Success! ------------------------------ Area: 15 Validation error: All arguments must be positive, got -2

When to Use Decorators

Decorators are useful for:

  • Logging function calls and timing performance
  • Validating function inputs
  • Adding authentication or permission checks
  • Caching function results
  • Retrying failed operations

This concept is particularly important when building web applications with Flask, where decorators are commonly used for routing and authentication.

Advanced Python Function Concepts - Lambda functions, decorators, and best practices

Click image to view full size

Decorator Challenge

Create a decorator called benchmark that measures and prints how long a function takes to run. Apply it to a function that calculates the factorial of a number.

Best Practices and Common Mistakes

Now that you know how to create functions, let's learn how to write them well. Good functions make your code easier to read, test, and maintain.

Writing Clear Function Names

Your function name should clearly explain what the function does. Someone should be able to understand the function's purpose just by reading its name:

Good vs Bad Function Names
# Bad function names (avoid these)
def calc(x, y):  # What kind of calculation?
    return x * y

def process_data(data):  # What kind of processing?
    return [item.upper() for item in data]

def check(password):  # Check for what?
    return len(password) >= 8

# Good function names (clear and descriptive)
def calculate_rectangle_area(length, width):
    return length * width

def convert_to_uppercase(text_list):
    return [item.upper() for item in text_list]

def is_password_strong_enough(password):
    return len(password) >= 8

# Examples of good naming patterns
def get_user_email(user_id):
    pass  # Returns email address

def save_customer_data(customer_info):
    pass  # Saves data to database

def validate_credit_card(card_number):
    pass  # Returns True/False

Keep Functions Small and Focused

Each function should do one thing well. If your function is doing multiple unrelated tasks, split it into smaller functions:

Bad: Function Doing Too Much
# Bad: This function does too many things
def process_order(customer_name, items, payment_method):
    # Calculate total
    total = 0
    for item in items:
        total += item['price'] * item['quantity']
    
    # Apply discount
    if total > 100:
        total *= 0.9
    
    # Process payment
    if payment_method == 'credit':
        print(f"Processing credit card payment of ${total}")
    elif payment_method == 'paypal':
        print(f"Processing PayPal payment of ${total}")
    
    # Send confirmation email
    print(f"Sending confirmation email to {customer_name}")
    
    # Update inventory
    for item in items:
        print(f"Reducing inventory for {item['name']}")
    
    return total
Good: Separated into Focused Functions
# Good: Each function has a single responsibility
def calculate_order_total(items):
    total = 0
    for item in items:
        total += item['price'] * item['quantity']
    return total

def apply_discount(total, minimum_amount=100, discount_rate=0.1):
    if total >= minimum_amount:
        return total * (1 - discount_rate)
    return total

def process_payment(total, payment_method):
    if payment_method == 'credit':
        print(f"Processing credit card payment of ${total:.2f}")
    elif payment_method == 'paypal':
        print(f"Processing PayPal payment of ${total:.2f}")
    else:
        raise ValueError(f"Unsupported payment method: {payment_method}")

def send_confirmation_email(customer_name, total):
    print(f"Sending confirmation email to {customer_name} for ${total:.2f}")

def update_inventory(items):
    for item in items:
        print(f"Reducing inventory for {item['name']} by {item['quantity']}")

def process_order(customer_name, items, payment_method):
    # Now this function orchestrates the process
    total = calculate_order_total(items)
    total = apply_discount(total)
    process_payment(total, payment_method)
    send_confirmation_email(customer_name, total)
    update_inventory(items)
    return total

# Test the improved version
sample_items = [
    {'name': 'Laptop', 'price': 800, 'quantity': 1},
    {'name': 'Mouse', 'price': 25, 'quantity': 2}
]

final_total = process_order("Alice Johnson", sample_items, "credit")
print(f"Order completed. Final total: ${final_total:.2f}")
Output:
Processing credit card payment of $765.00 Sending confirmation email to Alice Johnson for $765.00 Reducing inventory for Laptop by 1 Reducing inventory for Mouse by 2 Order completed. Final total: $765.00

Use Docstrings to Document Your Functions

Docstrings are special comments that describe what your function does. They help other programmers (and future you) understand your code:

Functions with Docstrings
def calculate_compound_interest(principal, rate, time, compounds_per_year=1):
    """
    Calculate compound interest on an investment.
    
    Args:
        principal (float): The initial amount of money
        rate (float): Annual interest rate (as a decimal, e.g., 0.05 for 5%)
        time (int): Number of years
        compounds_per_year (int): How many times per year interest is compounded
    
    Returns:
        float: The final amount after compound interest
    
    Example:
        >>> calculate_compound_interest(1000, 0.05, 10)
        1628.89
    """
    amount = principal * (1 + rate / compounds_per_year) ** (compounds_per_year * time)
    return round(amount, 2)

def validate_email(email):
    """
    Check if an email address has a valid format.
    
    Args:
        email (str): Email address to validate
    
    Returns:
        bool: True if email format is valid, False otherwise
    
    Note:
        This is a simple validation. For production use,
        consider using a proper email validation library.
    """
    return '@' in email and '.' in email.split('@')[1]

# Test the documented functions
investment = calculate_compound_interest(1000, 0.05, 10)
print(f"Investment result: ${investment}")

print(f"Is 'user@email.com' valid? {validate_email('user@email.com')}")
print(f"Is 'invalid-email' valid? {validate_email('invalid-email')}")

# You can access the docstring
print("\nFunction documentation:")
print(calculate_compound_interest.__doc__)
Output:
Investment result: $1628.89 Is 'user@email.com' valid? True Is 'invalid-email' valid? False Function documentation: Calculate compound interest on an investment. Args: principal (float): The initial amount of money rate (float): Annual interest rate (as a decimal, e.g., 0.05 for 5%) time (int): Number of years compounds_per_year (int): How many times per year interest is compounded Returns: float: The final amount after compound interest Example: >>> calculate_compound_interest(1000, 0.05, 10) 1628.89

Common Mistakes to Avoid

Mistake 1: Modifying Mutable Default Arguments

Never use mutable objects (like lists or dictionaries) as default parameters:

Mutable Default Arguments Problem
# Bad: Mutable default argument
def add_item_bad(item, shopping_list=[]):
    shopping_list.append(item)
    return shopping_list

# Good: Use None and create new list inside function
def add_item_good(item, shopping_list=None):
    if shopping_list is None:
        shopping_list = []
    shopping_list.append(item)
    return shopping_list

# Demonstrate the problem
print("Bad version:")
list1 = add_item_bad("apples")
print(f"First call: {list1}")

list2 = add_item_bad("bananas")  # This will have both items!
print(f"Second call: {list2}")

print("\nGood version:")
list3 = add_item_good("apples")
print(f"First call: {list3}")

list4 = add_item_good("bananas")  # This will only have bananas
print(f"Second call: {list4}")
Output:
Bad version: First call: ['apples'] Second call: ['apples', 'bananas'] Good version: First call: ['apples'] Second call: ['bananas']

Mistake 2: Not Handling Errors Properly

Always consider what could go wrong and handle errors gracefully:

Proper Error Handling
# Bad: No error handling
def divide_bad(a, b):
    return a / b

# Good: Proper error handling
def divide_good(a, b):
    """
    Divide two numbers safely.
    
    Args:
        a (float): Dividend
        b (float): Divisor
    
    Returns:
        float: Result of division, or None if division by zero
    """
    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
        print("Error: Both arguments must be numbers")
        return None
    
    if b == 0:
        print("Error: Cannot divide by zero")
        return None
    
    return a / b

# Test error handling
print("Testing division:")
print(f"10 / 2 = {divide_good(10, 2)}")
print(f"10 / 0 = {divide_good(10, 0)}")
print(f"'10' / 2 = {divide_good('10', 2)}")
Output:
Testing division: 10 / 2 = 5.0 Error: Cannot divide by zero 10 / 0 = None Error: Both arguments must be numbers '10' / 2 = None

Performance Tips

Understanding Python's built-in functions like the id() function and the type() function can help you write more efficient code and debug issues.

Function Performance Best Practices

  • Avoid unnecessary global variable access
  • Use local variables when possible
  • Don't create functions inside loops unless necessary
  • Consider using list comprehensions for simple transformations
  • Cache expensive calculations when appropriate

Code Review Exercise

Review this function and identify at least 3 improvements that could be made:

Function to Review
def calc(data, x=[], y=""):
    for i in data:
        x.append(i * 2)
    if y:
        x.append(y)
    return x

Interactive Quiz: Test Your Knowledge

Let's test what you've learned about Python functions! Try to answer these questions to reinforce your understanding.

Python Functions Quiz

Question 1: Function Definition

Which of the following is the correct way to define a function in Python?

Question 2: Return Values

What does a Python function return if it doesn't have a return statement?

Question 3: Lambda Functions

Which of these lambda functions correctly squares a number?

Question 4: Variable Scope

What will this code print?

x = 10
def test():
    x = 20
    print(x)

test()
print(x)

Question 5: Default Parameters

What's wrong with this function definition?

def greet(name, message="Hello", punctuation):

Want to test your Python skills further? Try this comprehensive Python coding quiz to challenge yourself with more advanced problems.

Frequently Asked Questions

What's the difference between parameters and arguments? +

Parameters are the variable names in the function definition (like def greet(name): where name is a parameter). Arguments are the actual values you pass when calling the function (like greet("Alice") where "Alice" is an argument).

Can a function call itself? +

Yes! This is called recursion. A function can call itself, but you need a base case to stop the recursion, otherwise it will run forever. For example:

def factorial(n):
    if n <= 1:  # Base case
        return 1
    return n * factorial(n - 1)  # Recursive call
How many parameters can a function have? +

Python doesn't have a strict limit, but practically you should keep functions simple. If you need many parameters, consider using a dictionary or creating a class. Generally, more than 5-7 parameters makes a function hard to use and understand.

What happens if I forget to return a value? +

If a function doesn't have a return statement, Python automatically returns None. This won't cause an error, but if you're expecting a value, you might get unexpected results in your program.

Can I modify global variables inside a function? +

Yes, but you need to use the global keyword. However, it's generally better to pass values as parameters and return results instead of modifying global variables, as this makes your functions more predictable and easier to test.

When should I use lambda functions vs regular functions? +

Use lambda functions for simple, one-line operations, especially with functions like map(), filter(), and sorted(). Use regular functions for anything complex, reusable, or when you want a descriptive name.

How do I test if my functions work correctly? +

Write test cases with different inputs and check if you get expected outputs. Python has a built-in unittest module, or you can use simple assert statements. Good functions are easy to test because they have clear inputs and outputs.

What are *args and **kwargs used for? +

*args allows a function to accept any number of positional arguments, while **kwargs allows any number of keyword arguments. They're useful when you don't know in advance how many arguments will be passed to your function.

Additional Resources for Learning

Continue your Python function learning journey with these excellent resources:

Practice Recommendations

The best way to master functions is through practice. Try these activities:

  • Rewrite some of your existing code using functions
  • Solve coding challenges on platforms like HackerRank or LeetCode
  • Build small projects that use different types of functions
  • Read other people's code to see how they structure their functions

Wrapping Up Your Function Journey

Congratulations! You've learned everything you need to know about Python functions, from the basics to advanced concepts. You now understand how to create functions, work with parameters, handle return values, manage variable scope, and even use advanced features like decorators and lambda functions.

Functions are the building blocks of well-organized Python programs. They help you write code that is reusable, easier to understand, and simpler to maintain. The concepts you've learned here will serve you well as you continue your Python programming journey.

Remember, becoming proficient with functions takes practice. Start by applying these concepts to your own projects, and don't be afraid to experiment with different approaches. The more you use functions, the more natural they'll become.

Next Steps in Your Python Learning

Now that you understand functions, you're ready to explore:

  • Object-oriented programming with classes
  • Working with modules and packages
  • File handling and data processing
  • Web development frameworks like Flask and Django
  • Data science libraries like NumPy and Pandas

Keep coding, keep learning, and most importantly, have fun with Python!

About The Author

Leave a Reply

Your email address will not be published. Required fields are marked *

  • Rating