How to create a function in Python

Functions in Python let you write reusable blocks of code that perform specific tasks. They help break down complex programs into manageable pieces, improve code organization, and follow the DRY (Don't Repeat Yourself) principle.

This guide covers essential techniques for creating robust Python functions, with practical examples and debugging tips created using Claude, an AI assistant built by Anthropic.

Basic function definition with def

def greet():
    print("Hello, World!")
    
greet()  # Call the function
Hello, World!

The def keyword creates a function definition in Python, establishing a reusable code block that you can call multiple times. This example demonstrates a simple function named greet() that outputs a greeting message.

Functions provide several key benefits for code organization and maintenance:

  • They encapsulate related code into logical units
  • They reduce code duplication by making functionality reusable
  • They improve readability by breaking complex operations into named components

When Python encounters the def statement, it creates a function object and assigns it to the specified name. The indented code block beneath defines what the function will do when called.

Function parameters and return values

Functions become more powerful when they can receive input data, work with default values, and handle flexible argument structures through *args and **kwargs.

Using parameters to pass data to functions

def add_numbers(a, b):
    return a + b
    
result = add_numbers(5, 3)
print(f"The sum is: {result}")
The sum is: 8

The add_numbers() function demonstrates how parameters enable functions to work with input data. When you call the function with add_numbers(5, 3), Python passes these values to the parameters a and b in the function definition.

  • The function takes two parameters that represent the numbers to add
  • The return statement sends the calculated sum back to where the function was called
  • The returned value gets stored in the result variable for later use

This pattern of passing data in, processing it, and returning results forms the foundation for writing flexible, reusable functions. You can call the same function with different values to perform the same operation on various inputs.

Using default parameter values

def greet_person(name, greeting="Hello"):
    return f"{greeting}, {name}!"
    
print(greet_person("Alice"))
print(greet_person("Bob", "Hi"))
Hello, Alice!
Hi, Bob!

Default parameters let you specify fallback values that Python uses when you don't provide an argument. In greet_person(), the greeting parameter defaults to "Hello" if you omit it when calling the function.

  • When calling greet_person("Alice"), Python uses the default greeting "Hello"
  • For greet_person("Bob", "Hi"), the explicit "Hi" argument overrides the default
  • Default values make functions more flexible while maintaining backward compatibility

You can set defaults for any parameter. However, parameters with default values must come after parameters without defaults in the function definition. This design choice helps Python avoid ambiguity when matching arguments to parameters.

Working with *args and **kwargs

def process_data(*args, **kwargs):
    print(f"Positional arguments: {args}")
    print(f"Keyword arguments: {kwargs}")
    
process_data(1, 2, 3, name="Alice", age=30)
Positional arguments: (1, 2, 3)
Keyword arguments: {'name': 'Alice', 'age': 30}

Python's *args and **kwargs syntax enables functions to accept any number of arguments with remarkable flexibility. The *args parameter collects all positional arguments into a tuple, while **kwargs gathers keyword arguments into a dictionary.

  • When process_data(1, 2, 3, name="Alice", age=30) runs, *args captures (1, 2, 3) as positional arguments
  • The **kwargs parameter simultaneously collects name and age as keyword arguments into a dictionary
  • This pattern proves especially useful when building functions that need to handle varying numbers of inputs or pass arguments to other functions

The asterisk operators (* and **) tell Python to unpack these collections of arguments. This makes your functions more adaptable without requiring fixed parameter counts.

Advanced function techniques

Building on these foundational concepts, Python offers powerful techniques like lambda functions, decorators, and recursion to write more sophisticated and efficient code.

Creating anonymous functions with lambda

square = lambda x: x ** 2
numbers = [1, 2, 3, 4, 5]
squared = list(map(square, numbers))
print(squared)
[1, 4, 9, 16, 25]

Lambda functions create small, anonymous functions in a single line of code. The example shows a lambda that takes one parameter x and returns its square using the power operator **.

  • The lambda syntax eliminates the need for a formal def statement when you need a simple function
  • The map() function applies our square lambda to each number in the list
  • Converting the map result to a list produces the final squared values

While lambdas work well for simple operations, they should only contain a single expression. Use regular functions for more complex logic that requires multiple lines or statements.

Implementing function decorators

def my_decorator(func):
    def wrapper():
        print("Before function call")
        func()
        print("After function call")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")
    
say_hello()
Before function call
Hello!
After function call

Decorators modify or enhance functions without changing their source code. The @my_decorator syntax wraps the say_hello() function inside the wrapper() function, enabling you to execute code before and after the original function runs.

  • The decorator function my_decorator takes a function as input and returns a new function that adds functionality
  • When Python sees @my_decorator, it automatically passes say_hello as an argument to my_decorator
  • The wrapper() function provides a clean way to extend behavior while maintaining the original function's integrity

This pattern proves especially useful for adding logging, performance monitoring, or access control to multiple functions without duplicating code. You'll often encounter decorators in web frameworks and testing tools.

Creating recursive functions

def factorial(n):
    if n <= 1:
        return 1
    else:
        return n * factorial(n-1)

print(f"5! = {factorial(5)}")
5! = 120

Recursive functions solve problems by calling themselves with modified inputs. The factorial() function demonstrates this elegant approach by calculating the product of a number and all positive integers below it.

  • The if n <= 1 statement serves as the base case. It stops the recursion when n reaches 1, preventing infinite loops
  • Each recursive call multiplies the current number n with factorial(n-1). This breaks down the calculation into smaller, manageable steps
  • When calculating factorial(5), the function creates a chain of operations: 5 * 4 * 3 * 2 * 1

While recursion offers an intuitive solution for mathematical operations like factorials, it can consume more memory than iterative approaches. Consider your specific use case when choosing between recursive and loop-based implementations.

Processing data with functions and multiple return values

Python functions can return multiple values in a single statement, enabling efficient data analysis through tuple packing and unpacking—as demonstrated in this temperature processing example.

def analyze_temperatures(temperatures):
    avg = sum(temperatures) / len(temperatures)
    highest = max(temperatures)
    lowest = min(temperatures)
    return avg, highest, lowest

daily_temps = [72, 65, 78, 80, 68, 74, 77]
avg_temp, max_temp, min_temp = analyze_temperatures(daily_temps)
print(f"Average: {avg_temp:.1f}°F, Highest: {max_temp}°F, Lowest: {min_temp}°F")

The analyze_temperatures function processes a list of temperature readings and returns three key metrics in a single line. It calculates the average using sum() divided by len(), while max() and min() identify the highest and lowest values.

Python's tuple unpacking shines in this example. When the function returns multiple values separated by commas, you can assign them directly to separate variables. The f-string formats the output with a single decimal place for the average temperature using .1f.

  • Takes a list of temperatures as input
  • Returns three values: average, maximum, minimum
  • Uses built-in Python functions for efficient calculations

Creating a function-based calculator with lambda and dictionaries

This example combines lambda functions with a dictionary to create a flexible calculator that performs basic arithmetic operations through a clean, maintainable interface.

def calculate(operation, a, b):
    operations = {
        'add': lambda x, y: x + y,
        'subtract': lambda x, y: x - y,
        'multiply': lambda x, y: x * y,
        'divide': lambda x, y: x / y if y != 0 else "Error: Division by zero"
    }
    
    if operation in operations:
        return operations[operation](a, b)
    return "Unknown operation"

print(calculate('add', 10, 5))
print(calculate('multiply', 3, 4))
print(calculate('divide', 8, 2))

The calculate() function implements a flexible calculator using a dictionary to store mathematical operations. Each operation maps to a lambda function that performs the calculation. When you call calculate('add', 10, 5), it looks up the 'add' operation in the dictionary and executes the corresponding lambda with the provided numbers.

  • The dictionary approach eliminates the need for multiple if/else statements
  • Each lambda function concisely defines the mathematical operation in a single line
  • The function handles division by zero gracefully by returning an error message

If you pass an operation that doesn't exist in the dictionary, the function returns "Unknown operation" instead of raising an error. This makes the function more robust and user-friendly.

Common errors and challenges

Python functions can trigger subtle bugs through scope confusion, mutable defaults, and recursive depth limits. Understanding these pitfalls helps you write more reliable code.

Avoiding variable scope issues with the global keyword

Variable scope issues commonly trip up Python developers when modifying global variables inside functions. The following code demonstrates a classic UnboundLocalError that occurs when trying to modify the global total variable without properly declaring it.

total = 0

def add_to_total(value):
    total = total + value
    return total

print(add_to_total(5))

Python interprets total = total + value as creating a new local variable instead of modifying the global one. This triggers an error because the function tries to use total before assigning it. Check out the corrected version below.

total = 0

def add_to_total(value):
    global total
    total = total + value
    return total

print(add_to_total(5))

The global keyword explicitly tells Python to use the variable from the global scope instead of creating a new local one. Without it, Python assumes you want to create a local variable when you assign a value, even if a global variable exists with the same name.

  • Watch for this issue when modifying global variables inside functions
  • Consider using return values and parameters instead of global variables for cleaner code
  • The error typically appears as UnboundLocalError when you try to read a global variable before assigning it locally

While the global keyword fixes the immediate problem, it's often better to avoid global variables altogether. They can make code harder to understand and maintain.

Avoiding unexpected behavior with mutable default arguments like [] and {}

Python's mutable default arguments can create confusing behavior when you reuse functions. The default list or dictionary persists between function calls instead of creating a fresh instance each time. This code demonstrates a common trap with list defaults.

def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item("apple"))
print(add_item("banana"))

The add_item() function creates a single list object when Python defines the function. Each call modifies this same list instead of creating a new one. The code below demonstrates the proper way to handle mutable default arguments.

def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item("apple"))
print(add_item("banana"))

Using None as the default argument and creating a new list inside the function solves the mutable default argument problem. This approach ensures each function call starts with a fresh list instead of reusing the same one.

  • Watch for this issue when using lists, dictionaries, or sets as default arguments
  • The problem occurs because Python evaluates default arguments only once during function definition
  • Always initialize mutable defaults to None and create the actual data structure inside the function

This pattern works especially well for functions that need to maintain separate state for each call. It prevents unexpected behavior where data from previous calls affects current operations.

Preventing stack overflow with function memoization in recursive calls

Recursive functions can quickly consume memory and processing power when handling large inputs. The classic fibonacci() implementation demonstrates this limitation. Each recursive call creates new stack frames that accumulate until Python hits its recursion limit or runs out of memory.

def fibonacci(n):
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(35))

The fibonacci() function recalculates the same values repeatedly. For example, computing fibonacci(5) calculates fibonacci(2) three separate times. This redundancy creates exponential time complexity and slows down the program significantly.

The optimized version below demonstrates how to cache previously calculated values.

def fibonacci(n, memo=None):
    if memo is None:
        memo = {}
    if n in memo:
        return memo[n]
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo)
    return memo[n]

print(fibonacci(35))

The optimized fibonacci() function uses memoization to store previously calculated values in a dictionary. This caching technique prevents redundant calculations by checking if a value exists in the memo before computing it again. The memo dictionary persists between recursive calls through the function parameter.

  • Watch for performance issues in recursive functions that compute the same values multiple times
  • Consider using memoization when your recursive function has overlapping subproblems
  • The time complexity improves from exponential to linear with this optimization

This pattern works particularly well for mathematical sequences and dynamic programming problems where values depend on previous results.

FAQs

What is the basic syntax for defining a function in Python?

Python functions start with the def keyword, followed by your chosen function name and parentheses containing any parameters. The function body uses indentation to show which code belongs to the function.

A simple function looks like this: def greet(name): followed by indented code. The colon signals the start of the function body. Python uses this clear syntax to make functions readable and maintainable.

How do you call a function after it's been defined?

To call a function after defining it, simply write the function's name followed by parentheses: myFunction(). The parentheses tell JavaScript to execute the function rather than just reference it. You can pass data into the function by adding arguments between the parentheses: myFunction("hello").

Functions work this way because they're first-class objects in JavaScript. This means you can store them in variables, pass them as arguments, or return them from other functions—giving you powerful ways to structure your code.

What's the difference between parameters and arguments in Python functions?

Parameters act as placeholders in function definitions, like def greet(name) where name is the parameter. Arguments are the actual values you pass when calling the function, such as greet("Alice"). Think of parameters as variables that outline what a function expects—arguments fill those variables with real data when the function runs.

This distinction matters because parameters define a function's interface while arguments provide the concrete input that makes the function do its work. Parameters create flexibility by letting you write reusable functions that can handle different inputs.

How do you make a function return a value using the 'return' statement?

Functions return values to their caller using the return statement, which immediately exits the function and sends back the specified value. When you write return x, the function packages up that value and passes it back to wherever the function was called from.

  • The returned value can be any data type: numbers, strings, objects, or even other functions
  • You can store the returned value in a variable or use it directly in other operations
  • A function stops executing as soon as it hits a return statement

Can you create a function without any parameters or return values?

Yes, you can create a function without parameters or return values. In Python, you'd define it using def function_name(): followed by the function body. These functions, often called procedures, perform actions like printing output or modifying global variables without explicitly returning data.

While possible, parameterless functions with no return values limit reusability and make testing harder. They work best for simple, self-contained tasks that don't need external inputs or produce specific outputs.

🏠