How to create a function in Python

Learn to create functions in Python. This guide covers different methods, tips, real-world applications, and how to debug common errors.

How to create a function in Python
Published on: 
Fri
Feb 6, 2026
Updated on: 
Tue
Feb 10, 2026
The Replit Team Logo Image
The Replit Team

Functions are a core part of Python development. They let you write reusable, organized code, which makes your programs much easier to manage and scale.

You'll learn techniques to define functions with the def keyword, explore real-world applications, and get practical tips to debug your code. This will help you build robust applications.

Basic function definition with def

def greet():
print("Hello, World!")

greet() # Call the function--OUTPUT--Hello, World!

The def keyword is how you declare a function, creating a reusable block of code. In this case, the function greet() encapsulates a single, specific task—printing a message. This practice of bundling instructions under a simple name is fundamental to writing clean, modular code that’s easy to read and manage.

You execute the code inside the function by calling it by its name, greet(). This runs the print() statement contained within it. The real advantage is reusability; you can call greet() from anywhere in your program to perform its action without rewriting the code each time.

Function parameters and return values

While the basic greet() function is a great start, you can make functions far more dynamic by passing in data with parameters and sending back results.

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}")--OUTPUT--The sum is: 8

The add_numbers function uses parameters a and b as placeholders for the data it will operate on. When you call add_numbers(5, 3), you're passing the numbers 5 and 3 into the function as arguments, making it dynamic.

  • The return statement sends the calculated sum back to the part of your code that made the call.
  • You can then capture this output in a variable—in this case, result—for later use.

Using default parameter values

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

print(greet_person("Alice"))
print(greet_person("Bob", "Hi"))--OUTPUT--Hello, Alice!
Hi, Bob!

Default parameters add flexibility by making some arguments optional. In the greet_person function, the greeting parameter is assigned a default value of "Hello" directly in the definition. This means you don't have to supply it every time you call the function.

  • When you call greet_person("Alice"), the function falls back on the default greeting because you only provided the required name argument.
  • You can easily override this default by supplying a second argument, as seen in greet_person("Bob", "Hi"), which uses your custom greeting instead.

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)--OUTPUT--Positional arguments: (1, 2, 3)
Keyword arguments: {'name': 'Alice', 'age': 30}

Sometimes you need a function that can handle an unknown number of arguments. That’s where *args and **kwargs come in, giving your functions the flexibility to accept a variable number of inputs without defining each parameter explicitly.

  • *args gathers all positional arguments into a tuple. In the process_data example, (1, 2, 3) are collected by *args.
  • **kwargs collects all keyword arguments into a dictionary. Here, name="Alice" and age=30 become {'name': 'Alice', 'age': 30}.

This powerful combination lets you build adaptable functions that can handle diverse inputs gracefully.

Advanced function techniques

Building on the basics, you can now tackle advanced techniques like anonymous lambda functions, decorators, and recursion to write more concise and powerful code.

Creating anonymous functions with lambda

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

A lambda function is a small, anonymous function you can define in a single line. It's perfect for simple operations where a full def statement feels like overkill. In this example, lambda x: x ** 2 creates a function that takes an argument x and returns its squared value, all without a formal name or return statement.

  • They are often used with higher-order functions like map(), which applies the lambda to every item in the numbers list.
  • This makes your code more compact, especially when the operation is only needed once.

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()--OUTPUT--Before function call
Hello!
After function call

Decorators are a powerful way to modify or extend a function's behavior without changing its source code. The @my_decorator syntax is a shortcut that "wraps" the say_hello function with additional logic from the my_decorator function.

  • The decorator accepts your original function (func) and returns a new wrapper function.
  • This wrapper executes its own code before and after calling your original function.
  • So when you call say_hello(), you're actually running the wrapper, which adds the extra print statements.

Creating recursive functions

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

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

A recursive function is one that calls itself to solve a problem by breaking it down into smaller, identical pieces. The factorial function demonstrates this perfectly—it keeps calling itself with a progressively smaller number until it hits a stopping point.

  • The base case, if n <= 1:, is the essential exit condition that prevents the function from running forever.
  • The recursive step, return n * factorial(n-1), is where the function calls itself, moving closer to the base case with each call.

Move faster with Replit

Replit is an AI-powered development platform that transforms natural language into working applications. With Replit Agent, you can build complete apps—with databases, APIs, and deployment—directly from a description.

For the function techniques we've explored, Replit Agent can turn them into production tools:

  • Build a dynamic calculator that uses *args to accept a variable number of inputs for operations like summing or averaging.
  • Create a data validation utility that uses decorators to automatically check function inputs, ensuring data integrity before processing.
  • Deploy a web scraper that uses recursive functions to navigate and extract information from nested page structures.

Describe your app idea, and Replit Agent writes the code, tests it, and fixes issues automatically, all in your browser.

Common errors and challenges

Even with a solid grasp of the basics, you'll run into common pitfalls like scope issues, mutable defaults, and recursion limits.

  • Avoiding variable scope issues with the global keyword. Variables created inside a function are local, meaning they only exist within that function's scope. If you need to modify a variable that lives outside the function, you must explicitly tell Python your intent. Using the global keyword before a variable name inside a function signals that you are referring to the global variable, not creating a new local one.
  • Avoiding unexpected behavior with mutable default arguments. Using mutable types like a list [] or dictionary {} as default arguments can cause bugs. Python creates the default object only once when the function is defined, not each time it's called. This means every call that uses the default will share and modify the exact same object, leading to surprising results. The standard practice is to use None as the default and create a new mutable object inside the function if the argument is None.
  • Preventing stack overflow with function memoization. Recursive functions that call themselves too many times can cause a stack overflow error. Memoization is a technique to optimize these functions by caching their results. When a function is called with certain inputs, it stores the result before returning it. If it's called again with the same inputs, it retrieves the saved result from the cache instead of re-computing it, which saves time and prevents excessively deep recursion.

Avoiding variable scope issues with the global keyword

A frequent error occurs when you try to modify a global variable like total from inside a function. Python protects the global scope by default, treating total as a new, unassigned local variable. This code shows the resulting error.

total = 0

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

print(add_to_total(5))

The assignment operator (=) makes total a local variable within the function. When Python tries to read total for the addition, it finds this local variable is unassigned, which causes an error. The corrected code shows the solution.

total = 0

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

print(add_to_total(5))

The global total line is the fix. It explicitly tells the add_to_total function to modify the total variable that exists outside its local scope. Without it, Python would try to create a new local total, causing an error since it's used before being assigned.

You'll need this keyword anytime a function must change a global variable's value—a common scenario in scripts that manage a shared state across different function calls.

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

Using a mutable object like a list ([]) or dictionary ({}) as a default argument is a classic Python "gotcha." It causes unexpected side effects because the default object is created only once, then shared across all subsequent function calls. The following code demonstrates this problem in action.

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

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

The second call to add_item doesn't get a new list. It appends "banana" to the same list that already holds "apple" from the first call. The corrected code below shows how to fix this behavior.

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

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

The fix is to set the default argument to None instead of an empty list []. Inside the add_item function, a check for None creates a new list for each call that doesn't provide one. This pattern guarantees that each function call operates on its own independent list, avoiding unintended side effects. Always use this approach when a function's default argument is a mutable type like a list or dictionary.

Preventing stack overflow with function memoization in recursive calls

Recursive functions can be inefficient, especially when they're forced to recalculate the same values repeatedly. This repetition doesn't just slow your program down; it can also cause a "stack overflow" error if the recursion goes too deep. The following fibonacci function demonstrates this problem.

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 expression fibonacci(n-1) + fibonacci(n-2) causes an exponential number of function calls, forcing the program to re-calculate the same values repeatedly. This inefficiency is what causes the slowdown. The corrected code shows a better way.

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 corrected fibonacci function uses memoization to speed up execution by storing results in a memo dictionary, which acts as a cache. Before calculating a value, the function checks if the result for n is already in the cache. If so, it returns the stored value immediately.

This avoids redundant calculations and prevents stack overflow errors. You should use this pattern for any recursive function that repeatedly calls itself with the same inputs.

Real-world applications

With a firm handle on function mechanics and error handling, you can now build practical tools for data processing and calculations.

Processing data with functions and multiple return values

Functions can return multiple values at once, which is an efficient way to get a bundle of related results—like key statistics from a dataset—in a single call.

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 efficiently processes a list of numbers by leveraging Python's built-in tools. It uses sum(), len(), max(), and min() to quickly compute key statistics without needing manual loops.

When you call the function, the results are returned together. The line avg_temp, max_temp, min_temp = analyze_temperatures(daily_temps) demonstrates a neat feature called tuple unpacking. This lets you assign each returned value to its own descriptive variable in a single, clean statement, making your code more readable.

Creating a function-based calculator with lambda and dictionaries

You can combine lambda functions with a dictionary to create a dynamic calculator that maps string commands to specific operations.

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 uses a dictionary to select an operation based on a string input. This technique, often called a dispatcher, is a clean alternative to writing multiple if/elif statements. The operations dictionary maps string keys like 'add' to anonymous lambda functions that perform the actual math.

When you call the function, it finds the matching operation by its key and executes the corresponding lambda with your numbers. This makes your code highly organized and simple to expand with new commands.

Get started with Replit

Put your new skills to work and build a real tool. Describe what you want to Replit Agent, like “a web app that converts units using a dictionary of lambda functions” or “a script that analyzes a dataset.”

Replit Agent writes the code, tests for errors, and deploys your application for you. Start building with Replit.

Get started free

Create and deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.

Get started for free

Create & deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.