Skip to content

Functions

A function is a reusable block of code with a name. Define once, use many times.

Defining a function

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

# Call the function
greet()
greet()
greet()

def defines a function. The indented block is its body. The function doesn't run until you call it with ().

Functions with parameters

Parameters are inputs to the function.

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

greet("Alice")
greet("Bob")

Multiple parameters

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

result = add(5, 3)
print(result)         # 8
print(add(10, 20))    # 30

return sends a value back to the caller. Without return, a function returns None.

Default parameter values

Make a parameter optional by giving it a default:

def greet(name, greeting="Hello"):
    print(f"{greeting}, {name}!")

greet("Alice")                       # Hello, Alice!
greet("Bob", "Hi")                   # Hi, Bob!
greet("Charlie", greeting="Yo")      # Yo, Charlie!

Keyword arguments

Pass arguments by name — order doesn't matter:

def create_user(name, age, city):
    print(f"{name}, {age} years old, lives in {city}")

# Positional — order matters
create_user("Alice", 25, "Mumbai")

# Keyword — clearer, order doesn't matter
create_user(age=25, city="Mumbai", name="Alice")

Variable-length arguments — *args and **kwargs

When you don't know how many arguments will be passed:

# *args collects extra positional args as a tuple
def add_all(*numbers):
    return sum(numbers)

print(add_all(1, 2, 3))               # 6
print(add_all(1, 2, 3, 4, 5, 10))     # 25

# **kwargs collects extra keyword args as a dict
def print_info(**details):
    for key, value in details.items():
        print(f"{key}: {value}")

print_info(name="Alice", age=25, city="Mumbai")

Returning multiple values

def min_max(numbers):
    return min(numbers), max(numbers)

low, high = min_max([3, 1, 4, 1, 5, 9, 2, 6])
print(f"Min: {low}, Max: {high}")

You're really returning a tuple that gets unpacked.

Docstrings — document your function

A string right after def is the function's docstring. help() shows it.

def area_of_circle(radius):
    """
    Return the area of a circle.

    Args:
        radius: the radius of the circle in any unit

    Returns:
        the area in unit²
    """
    return 3.14159 * radius * radius

print(area_of_circle(5))
help(area_of_circle)

Scope — local vs global variables

Variables inside a function are local — they only exist inside that function.

x = 10        # global

def show():
    x = 99    # local — different from the global x
    print("inside:", x)

show()
print("outside:", x)

To modify a global from inside a function, use global:

count = 0

def increment():
    global count
    count += 1

increment()
increment()
increment()
print(count)    # 3

Avoid global when possible. Better: return the value and let the caller assign it.

Recursion — a function that calls itself

A classic example — computing factorial: n! = n × (n-1)!

def factorial(n):
    if n <= 1:
        return 1                          # base case
    return n * factorial(n - 1)           # recursive case

print(factorial(5))    # 5*4*3*2*1 = 120
print(factorial(7))    # 5040

Every recursive function needs: 1. Base case — stops the recursion. 2. Recursive case — calls itself with a smaller problem.

Without a base case → infinite recursion → RecursionError.

lambda — anonymous one-liner functions

lambda parameters: expression — a function without a name, useful as a one-time argument.

# Same as: def square(x): return x * x
square = lambda x: x * x
print(square(5))     # 25

# Most useful when passing to another function
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
sorted_desc = sorted(numbers, key=lambda x: -x)
print(sorted_desc)

map, filter, reduce

Three functional-programming building blocks.

map — apply a function to every item:

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

# Same with a list comprehension (more Pythonic)
squares2 = [x * x for x in numbers]
print(squares2)

filter — keep only items where the function returns True:

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)    # [2, 4, 6, 8, 10]

# Same with comprehension
evens2 = [x for x in numbers if x % 2 == 0]
print(evens2)

reduce — combine all items into one value (need to import):

from functools import reduce

numbers = [1, 2, 3, 4, 5]
total = reduce(lambda acc, x: acc + x, numbers)
print(total)    # 1+2+3+4+5 = 15

product = reduce(lambda acc, x: acc * x, numbers)
print(product)    # 1*2*3*4*5 = 120

Practice

What does this print?

Expected: Hi, Alice!

def greet(name, greeting="Hi"):
    return f"{greeting}, {name}!"

print(greet("Alice"))

Return the sum of all args (not just the first one)

Expected: 10

def total(*numbers):
    return numbers[0]        # bug: this only returns the first
print(total(1, 2, 3, 4))

Quiz — Quick check

What you remember

Q1. What does a function return if you don't write a return statement?

  • 0
  • False
  • None
  • An error

Why: Every Python function implicitly returns None if no return is reached.

Q2. What does *args collect arguments into?

  • A list
  • A tuple
  • A dict
  • A set

Why: *args packs extra positional arguments into a tuple. **kwargs packs extra keyword arguments into a dict.

Q3. A recursive function must have a…

  • return of None
  • global declaration
  • base case
  • lambda form

Why: Without a base case (a stopping condition), the function keeps calling itself until Python raises RecursionError.

Common doubts

What's the difference between a parameter and an argument?

A parameter is the name in the function definition (def add(a, b) → a and b). An argument is the value you pass when calling (add(5, 3) → 5 and 3). People often use the terms interchangeably; in interviews you'll hear "parameters" for definitions and "arguments" for calls.

When should I use lambda?

Use lambda when you need a tiny throwaway function inline — typically as the key= argument to sorted, max, min, or with map/filter. For anything multi-line or reused, write a real def — it's more readable and easier to debug.

Are default arguments evaluated once or every call?

Once, when the function is defined. This causes a classic bug:

def add_to(item, target=[]):   # ← target is shared across calls!
    target.append(item)
    return target
Calling add_to(1) twice gives [1, 1], not [1] each time. Use target=None and create [] inside the function instead.

What's next

→ Strings