Toggle Menu Icon
Working with Python

Functions

Functions are one of the core building blocks of any language. Here are some examples of how to use functions in Python

Basic Function Definition

The simplest function looks like this:

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

greet()  # Call the function

Functions are defined with the def keyword, followed by the function name and parentheses. The colon starts the function body, the content of the function must be indented.

Functions with Parameters

To pass a parameter as input to a function:

def greet_person(name):
    print(f"Hello, {name}")

greet_person("Alice")
# Output: Hello, Alice

A function with multiple parameters:

def intro(name, city):
    print(f"Hi, I'm {name} from {city}")

intro("Sarah", "New York")
# Output: Hi, I'm Sarah from New York

Default parameters are specified with the parameter name:

def announce_score(team="Home", score=0):
    print(f"{team} team has scored {score} points")

# Usage examples
announce_score("Warriors", 102)
# Output: Warriors team has scored 102 points

announce_score("Lakers")
# Output: Lakers team has scored 0 points

announce_score()
# Output: Home team has scored 0 points

A parameter with a default value is an optional parameter, a parameter without a default value is a required parameter. You can mix required and optional parameters, but optional parameters must come last:

def create_user(username, email, role="user", active=True):
    return {
        "username": username,
        "email": email,
        "role": role,
        "active": active
    }

user1 = create_user("alice", "alice@example.com")

If you call a function without providing all required arguments, Python will raise a TypeError:

def intro(name, city):
    print(f"Hi, I'm {name} from {city}")

intro("Sarah")

# Traceback (most recent call last):
#  File "<stdin>", line 1, in <module>
# TypeError: intro() missing 1 required positional argument: 'city'

👉 Your Turn: Before we move on, try writing a function called describe_pet() that takes a pet’s name and animal type, with animal type defaulting to “dog”. Make it return a string like “Buddy is a friendly dog.”

Return Values

Use the return statement to return values from functions:

def add(a, b):
    return a + b

result = add(5, 3)
print(result)
# Output: 8

Without an explicit return, functions return None:

def say_hello():
    print("Hello")

result = say_hello()
# Output: Hello

print(result)
# Output: None

Multiple Return Values

Python makes it easy to return multiple values using tuples:

def get_name_parts(full_name):
    parts = full_name.split()
    first_name = parts[0]
    last_name = parts[-1]
    return first_name, last_name

first, last = get_name_parts("John Doe")
print(f"First: {first}, Last: {last}")
# Output: First: John, Last: Doe

Keyword Arguments

You can call functions using parameter names, this makes code easier to read since you don’t have to remember what order the arguments are in:

def create_rectangle(width, height, color="blue"):
    return f"A {color} rectangle: {width}x{height}"

# These are all equivalent:
rect1 = create_rectangle(10, 5)
rect2 = create_rectangle(width=10, height=5)
rect3 = create_rectangle(height=5, width=10)  # Order doesn't matter with keywords
rect4 = create_rectangle(10, height=5, color="red")  # Mix positional and keyword

Variable Arguments (*args)

Sometimes you don’t know how many arguments you’ll get, use * before the parameter name to accept any number of positional arguments, this creates a tuple of arguments assigned to the parameter name.

def sum_all(*numbers):
    total = 0
    for num in numbers:
        total += num
    return total

print(sum_all(1, 2, 3))        # Output: 6
print(sum_all(1, 2, 3, 4, 5))  # Output: 15
print(sum_all())               # Output: 0

Keyword Variable Arguments (**kwargs)

For flexible keyword arguments use two ** with the parameter name, this creates a dictionary of keyword arguments assigned to the parameter name.

def create_profile(**info):
    profile = {}
    for key, value in info.items():
        profile[key] = value
    return profile

user = create_profile(name="Alice", age=30, city="Boston", job="Engineer")
print(user)
# Output: {'name': 'Alice', 'age': 30, 'city': 'Boston', 'job': 'Engineer'}

You can combine all argument types, but I wouldn’t recommend it. To make code easiest to understand and most flexible, use keyword arguments.

def flexible_function(required_arg, default_arg="default", *args, **kwargs):
    print(f"Required: {required_arg}")
    print(f"Default: {default_arg}")
    print(f"Extra args: {args}")
    print(f"Keyword args: {kwargs}")

flexible_function("hello", "world", 1, 2, 3, name="Alice", age=25)

# Output:
# Required: hello
# Default: world
# Extra args: (1, 2, 3)
# Keyword args: {'name': 'Alice', 'age': 25}

Lambda Functions

For simple, one-line functions, you can use a lambda function:

# Regular function
def square(x):
    return x * x

# Lambda equivalent
square_lambda = lambda x: x * x

print(square(5))        # Output: 25
print(square_lambda(5)) # Output: 25

Lambdas are most useful with functions like map(), filter(), and sorted():

numbers = [1, 2, 3, 4, 5]

# Square all numbers
squared = list(map(lambda x: x * x, numbers))
print(squared)
# Output: [1, 4, 9, 16, 25]

# Filter even numbers
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)
# Output: [2, 4]

# Sort by custom criteria
people = [("Alice", 25), ("Bob", 30), ("Charlie", 20)]
sorted_by_age = sorted(people, key=lambda person: person[1])
print(sorted_by_age)
# Output: [('Charlie', 20), ('Alice', 25), ('Bob', 30)]

Function Documentation

Document functions using docstrings:

def calculate_compound_interest(principal, rate, time, compound_frequency=1):
    """
    Calculate compound interest.

    Args:
        principal (float): Initial amount of money
        rate (float): Annual interest rate (as decimal, e.g., 0.05 for 5%)
        time (float): Time in years
        compound_frequency (int): How many times interest compounds per year

    Returns:
        tuple: (final_amount, interest_earned)

    Example:
        >>> amount, interest = calculate_compound_interest(1000, 0.05, 2)
        >>> print(f"Amount: ${amount:.2f}, Interest: ${interest:.2f}")
    """
    final_amount = principal * (1 + rate/compound_frequency) ** (compound_frequency * time)
    interest_earned = final_amount - principal
    return final_amount, interest_earned

# Usage
amount, interest = calculate_compound_interest(1000, 0.05, 2, 4)
print(f"Final amount: ${amount:.2f}")
print(f"Interest earned: ${interest:.2f}")

Nested Functions

You can define functions inside other functions. A primary reason for this is to create closures. A closure allows the inner function to remember and access variables from the outer function’s scope, even after the outer function has finished executing. This pattern is useful for creating function factories or specialized functions tailored by parameters passed to the outer function.

Here’s a practical example of a closure - creating a counter function:

def create_counter(start=0):
    count = start

    def counter():
        nonlocal count
        count += 1
        return count

    return counter

# Create different counters
page_counter = create_counter(1)
error_counter = create_counter(0)

print(page_counter())  # Output: 2
print(page_counter())  # Output: 3
print(error_counter()) # Output: 1
print(page_counter())  # Output: 4

The inner counter function “closes over” the count variable from the outer function’s scope. Each time we call create_counter(), it creates a new closure with its own count variable that persists between calls to the returned function.

Tips on Using Functions

Here are the patterns I aim to follow when writing functions:

  1. Keep functions small and focused - each function should do one thing well
  2. Use descriptive names - calculate_tax() is better than calc()
  3. Return early when possible - avoid deep nesting
  4. Use type hints for clarity, see Type Hints chapter