Python Functions — A Complete Guide with Real-World Examples

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Introduction

Why This Matters

Functions are the fundamental unit of code reuse and abstraction in Python. Every Python program of any real complexity is built from functions — they organize logic, make code testable, and enable clean separation of concerns. Understanding Python's function system deeply, including its unique features like *args, **kwargs, default arguments, closures, and first-class function support, is essential for writing idiomatic Python.

Functions also interact with nearly every other Python feature: decorators wrap functions, generators use yield inside functions, comprehensions define inline functions, and lambda creates anonymous functions. Understanding functions well unlocks the rest of the language.

In technical interviews, questions about closures, mutable defaults, and argument passing are common and trip up candidates who haven't thought carefully about these concepts.

Defining and Calling Functions

def greet(name: str) -> str:
    """Return a personalized greeting."""
    return f"Hello, {name}!"
 
result = greet("Alice")
print(result)  # Hello, Alice!

Arguments: Positional, Keyword, and Default

def create_user(name: str, age: int, role: str = "user") -> dict:
    return {"name": name, "age": age, "role": role}
 
# Positional
create_user("Alice", 30)
 
# Keyword (order doesn't matter)
create_user(age=25, name="Bob")
 
# Override default
create_user("Charlie", 35, role="admin")

*args and **kwargs

def sum_all(*args: int) -> int:
    """Accept any number of positional arguments."""
    return sum(args)
 
print(sum_all(1, 2, 3, 4, 5))  # 15
 
def configure(**kwargs: str) -> None:
    """Accept any number of keyword arguments."""
    for key, value in kwargs.items():
        print(f"{key} = {value}")
 
configure(host="localhost", port="5432", db="mydb")
 
# Combine both
def mixed(required: str, *args, **kwargs):
    print(required, args, kwargs)
 
mixed("hello", 1, 2, x=10, y=20)
# hello (1, 2) {'x': 10, 'y': 20}

Unpacking Arguments

def add(a: int, b: int, c: int) -> int:
    return a + b + c
 
nums = [1, 2, 3]
print(add(*nums))   # 6 — unpack list as positional args
 
params = {"a": 1, "b": 2, "c": 3}
print(add(**params))  # 6 — unpack dict as keyword args

Return Multiple Values

Python functions can return multiple values as a tuple.

def minmax(numbers: list[int]) -> tuple[int, int]:
    return min(numbers), max(numbers)
 
low, high = minmax([3, 1, 4, 1, 5, 9, 2, 6])
print(f"Min: {low}, Max: {high}")  # Min: 1, Max: 9

Mutable Default Argument Gotcha

# BUG: mutable default is shared across all calls
def add_item_bad(item, items=[]):
    items.append(item)
    return items
 
print(add_item_bad("a"))  # ['a']
print(add_item_bad("b"))  # ['a', 'b'] -- BUG: should be ['b']
 
# FIX: use None as default, create inside
def add_item_good(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

Lambda Functions

# Anonymous one-liner functions
square = lambda x: x ** 2
print(square(5))  # 25
 
# Common use: sorting with a key
users = [{"name": "Charlie", "age": 35}, {"name": "Alice", "age": 30}]
sorted_users = sorted(users, key=lambda u: u["age"])
 
# In map/filter
numbers = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, numbers))
evens = list(filter(lambda x: x % 2 == 0, numbers))

Closures

A closure is a function that captures variables from its enclosing scope.

def make_multiplier(factor: int):
    def multiply(x: int) -> int:
        return x * factor  # 'factor' is captured from outer scope
    return multiply
 
double = make_multiplier(2)
triple = make_multiplier(3)
 
print(double(5))  # 10
print(triple(5))  # 15

First-Class Functions

In Python, functions are objects — they can be passed, stored, and returned.

from typing import Callable
 
def apply(func: Callable[[int], int], value: int) -> int:
    return func(value)
 
def square(x: int) -> int:
    return x ** 2
 
print(apply(square, 4))  # 16
 
# Store in a list
operations = [abs, str, lambda x: x * 2]
for op in operations:
    print(op(-5))  # 5, '-5', -10

Common Mistakes

  • Using mutable defaults (lists, dicts) in function signatures
  • Forgetting that inner functions form closures over variables, not values (late binding)
  • Overloading *args/**kwargs when explicit parameters are clearer
  • Using lambda for complex multi-line logic — define a named function instead
  • Not using type hints, making functions harder to read and maintain

Best Practices

  • Keep functions short and focused on a single responsibility
  • Use type hints on all public functions for readability and tooling support
  • Prefer explicit parameter names over *args for clarity
  • Use functools.wraps when wrapping functions in decorators
  • Write docstrings for all public functions following PEP 257

Key Takeaways

  • def defines a named function; lambda defines an anonymous one-liner
  • Default arguments are evaluated once at definition time — never use mutables as defaults
  • *args collects extra positional arguments into a tuple; **kwargs collects keyword args into a dict
  • Functions are first-class objects in Python — they can be passed, stored, and returned
  • Closures capture variables from enclosing scopes, enabling factory functions and callbacks
  • return can return multiple values as a tuple, which can be unpacked by the caller
  • Type hints do not enforce types at runtime — use Pydantic or isinstance() for validation

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro