Python 3.12+ Β· Updated June 2026

Python Functions
Reusable Code, Done Right

Master Python functions – from definition and parameters to scope, lambda, recursion, and decorators. Over 50 runnable examples, each explained in depth: why it's used, how it operates, how it affects your program, how it shapes the outcome, and why it matters.

functions_demo.py
def greet(name: str, greeting: str = "Hello") -> str:
    return f"{greeting}, {name}!"

print(greet("Glenn"))
print(greet("Ana", "Hola"))

print(greet("Bob", greeting="Bonjour"))
$ python functions_demo.py
Hello, Glenn!
Hola, Ana!
Bonjour, Bob!
Pyodide loading…

πŸ“– What Is a Python Function?

A function is a reusable block of code that performs a specific task. Functions are the fundamental building blocks of modular, maintainable Python programs β€” they allow you to write once, call many times, and they are the primary mechanism for abstraction, reuse, and organisation in Python.

Why Every Programmer Must Learn This Curriculum

Functions are the atomic unit of organisation in Python. Every program you'll ever write β€” from a simple script to a complex web application β€” is built from functions. They are the primary tool for abstraction (hiding complexity), reuse (writing code once and using it many times), and testing (isolating and verifying behaviour).

This curriculum is sequenced from first principles to advanced patterns. You'll start with how functions are defined and called, move through every parameter type (positional, keyword, default, *args, **kwargs), understand the LEGB scope rule, master lambda functions for concise callbacks, explore recursion for elegant problem-solving, and finish with decorators β€” the advanced pattern that powers frameworks like Flask, Django, and pytest.

By the end, you'll understand not just the syntax but the why: why functions are the heart of modular programming, why scope matters for correctness, why lambda functions exist, and why decorators are one of Python's most powerful features.

50+ runnable examples 8+ parameter patterns Beginner to Advanced 2.5–3 hours to complete

🎯 What You Will Learn

  • How functions are defined with def, and how they are called
  • Function parameters: positional, keyword, default, *args, and **kwargs
  • Return values β€” single values, multiple values (via tuples), and None
  • The LEGB scope rule β€” Local, Enclosing, Global, Built-in
  • lambda functions β€” concise, anonymous functions for callbacks
  • Recursion β€” functions that call themselves, and when to use them
  • Decorators β€” the advanced pattern for wrapping and extending functions
  • Docstrings and type annotations for self-documenting code
  • Three real-world mini projects that combine everything above

🌍 The Significance of Functions in the Programming World

Functions are the fundamental unit of procedural abstraction. Every programming language supports them, and they are the primary mechanism for structuring code in every paradigm β€” procedural, object-oriented, and functional. They are the noun and verb of your program: the things you do and the actions you perform.

  • Code reuse β€” write once, use many times, across different parts of your program
  • Abstraction β€” hide implementation details behind a simple interface
  • Testing β€” isolate and verify behaviour in small, testable units
  • Organisation β€” break large problems into smaller, manageable pieces
  • Readability β€” give meaningful names to blocks of code
  • Composability β€” build complex behaviour from simple, well-defined functions

Mastering functions means you'll write code that is more organised, more reusable, and easier to test β€” the foundation of professional software development.

πŸ€” Why Programmers Should Master Python Functions

Because functions are the fundamental unit of code organisation

🧩 Functions Are the Building Blocks of Every Program

Every program you write is a sequence of instructions β€” but without functions, that sequence becomes a long, unstructured, unreadable mess. Functions let you group related instructions, give them a name, and reuse them. They are the primary tool for abstraction β€” hiding complexity behind a simple interface.

  • Reuse β€” write a function once and call it from anywhere
  • Readability β€” calculate_tax(amount) is clearer than inline tax logic
  • Testing β€” functions are the natural unit of testing
  • Maintainability β€” fix a bug in one place, not everywhere
  • Composition β€” build complex behaviour from simple functions
πŸ’‘ If you find yourself copying and pasting the same code in multiple places, that's a signal you need a function. Write once, call many times.

πŸš€ Functions Are the Key to Professional Python

Professional Python code is organised into functions, modules, and packages. Functions are the smallest unit of organisation, and they are the primary mechanism for separation of concerns β€” each function does one thing and does it well. This makes code easier to understand, easier to test, and easier to change.

πŸ”€ Function Basics – Definition, Calls, and Return

The foundation everything else in this tutorial builds on

πŸ“ Defining and Calling Functions

The def Keyword and Function Calls

A function is defined with the def keyword, followed by the function name, parentheses, and a colon. The function body is indented. A function is called by using its name followed by parentheses.

# Example 1: defining a simple function
def say_hello():
    print("Hello, world!")

# Example 2: calling the function
say_hello()

# Example 3: functions with parameters
def greet(name):
    print(f"Hello, {name}!")

greet("Glenn")

# Example 4: functions with a return value
def add(a, b):
    return a + b

result = add(3, 5)
print(result)
πŸ” Why this code is used: Defining and calling functions is the most fundamental operation in Python β€” every program uses functions.

βš™οΈ How it operates: def creates a function object and assigns it to the function name. The code inside the function body runs only when the function is called. Parameters are variables that receive arguments when the function is called.

πŸ“Š How it affects the program: Example 4 shows that a function can return a value using the return statement β€” the caller can then use that value.

🎯 How it stimulates the outcome: Functions without an explicit return statement return None by default β€” a subtle but important point covered later.

πŸ“Œ Significance: The def statement is the primary mechanism for code organisation in Python β€” everything else is built on top of it.
πŸ“€ Return Values

The return Statement and None

# Example 5: returning a single value
def square(n):
    return n ** 2

print(square(5))

# Example 6: returning multiple values as a tuple
def min_max(numbers):
    return min(numbers), max(numbers)

low, high = min_max([1, 2, 3, 4, 5])
print(f"min: {low}, max: {high}")

# Example 7: functions return None by default
def no_return():
    print("I don't return anything")

result = no_return()
print(result)  # None

# Example 8: early return
def is_even(n):
    if n % 2 == 0:
        return True
    return False

print(is_even(4))
print(is_even(5))
πŸ” Why this code is used: Functions return values to communicate results back to the caller β€” this is how data flows through your program.

βš™οΈ How it operates: The return statement exits the function and sends a value back to the caller. If no return is encountered, the function returns None. A function can return multiple values as a tuple (Example 6), which the caller can unpack.

πŸ“Š How it affects the program: Example 7 demonstrates that a function without a return statement still returns something β€” None. This is a frequent source of confusion for beginners.

🎯 How it stimulates the outcome: Example 8 shows that return can be used early β€” it exits the function immediately, which is useful for guard clauses and early exits.

πŸ“Œ Significance: The return statement is how functions communicate β€” mastering it is essential for writing useful functions.
πŸ“ Docstrings

Documenting Functions with Docstrings

# Example 9: a function with a docstring
def calculate_area(length: float, width: float) -> float:
    """Calculate the area of a rectangle.

    Args:
        length: The length of the rectangle.
        width: The width of the rectangle.

    Returns:
        The area of the rectangle (length * width).
    """
    return length * width

print(calculate_area(5, 3))
print(calculate_area.__doc__)
πŸ” Why this code is used: Docstrings are the standard way to document functions β€” they are read by help(), IDEs, and documentation generators.

βš™οΈ How it operates: A docstring is a string literal that appears as the first statement in a function. It is stored in the function's __doc__ attribute and is displayed by help().

πŸ“Š How it affects the program: Example 9 shows that the docstring is accessible via calculate_area.__doc__ β€” this is how IDEs show function documentation.

🎯 How it stimulates the outcome: Writing docstrings is a professional habit β€” it makes your code more usable and maintainable, especially in team settings.

πŸ“Œ Significance: Docstrings are the primary documentation mechanism in Python β€” they are not optional in professional code.

πŸ“₯ Function Parameters β€” Positional, Keyword, Default, *args, **kwargs

Every way to pass data into a function

πŸ“₯ Positional and Keyword Arguments

Positional, Keyword, and Default Parameters

# Example 10: positional arguments β€” order matters
def describe_person(name, age, city):
    return f"{name} is {age} years old and lives in {city}"

print(describe_person("Glenn", 30, "Diplahan"))

# Example 11: keyword arguments β€” order doesn't matter
print(describe_person(age=30, city="Diplahan", name="Glenn"))

# Example 12: default parameters
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(greet("Ana"))
print(greet("Bob", "Hola"))

# Example 13: default parameters must come after non-default
# def bad(name="Glenn", age):  # SyntaxError
#     pass

def good(age, name="Glenn"):
    pass
πŸ” Why this code is used: Parameters are how functions receive data β€” understanding the different types gives you flexibility in how you call functions.

βš™οΈ How it operates: Positional arguments are matched by position β€” the first argument goes to the first parameter. Keyword arguments are matched by name β€” order doesn't matter. Default parameters provide a fallback value if the caller doesn't supply one.

πŸ“Š How it affects the program: Example 11 shows that keyword arguments make function calls more readable and self-documenting β€” describe_person(age=30, city="Diplahan", name="Glenn") is clearer than positional ordering.

🎯 How it stimulates the outcome: Default parameters are ideal for optional values β€” they let you call a function with fewer arguments when the defaults are sufficient.

πŸ“Œ Significance: Mastering positional, keyword, and default parameters gives you control over how functions are called and how they behave.
⭐ *args and **kwargs

Variable-Length Arguments

# Example 14: *args collects extra positional arguments as a tuple
def sum_all(*args):
    return sum(args)

print(sum_all(1, 2, 3, 4, 5))

# Example 15: **kwargs collects extra keyword arguments as a dict
def print_kwargs(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_kwargs(name="Glenn", age=30, city="Diplahan")

# Example 16: combining positional, *args, and **kwargs
def flexible(required, *args, **kwargs):
    print(f"Required: {required}")
    print(f"Extra args: {args}")
    print(f"Extra kwargs: {kwargs}")

flexible("must have", 1, 2, 3, name="Glenn", age=30)

# Example 17: unpacking a list into *args
numbers = [1, 2, 3, 4, 5]
print(sum_all(*numbers))

# Example 18: unpacking a dict into **kwargs
data = {"name": "Glenn", "age": 30}
print_kwargs(**data)
πŸ” Why this code is used: *args and **kwargs make functions flexible β€” they can accept any number of arguments, which is essential for decorators, wrappers, and generic functions.

βš™οΈ How it operates: *args collects any extra positional arguments into a tuple. **kwargs collects any extra keyword arguments into a dictionary. The names args and kwargs are conventions, not keywords β€” you could use *things and **stuff.

πŸ“Š How it affects the program: Example 16 shows the complete signature pattern β€” required parameters, then *args, then **kwargs.

🎯 How it stimulates the outcome: Examples 17 and 18 show the "unpacking" pattern β€” using * and ** to pass a list or dictionary as arguments to a function.

πŸ“Œ Significance: *args and **kwargs are used heavily in libraries and frameworks β€” they enable the flexible, extensible APIs that Python is known for.
πŸ”€ Keyword-Only and Positional-Only Parameters

Python 3.8+ Parameter Syntax

# Example 19: positional-only parameters using /
def pos_only(a, b, /, c):
    return a + b + c

print(pos_only(1, 2, 3))
# print(pos_only(1, 2, c=3))  # Valid
# print(pos_only(a=1, b=2, c=3))  # TypeError β€” a and b are positional-only

# Example 20: keyword-only parameters using *
def kw_only(a, *, b, c):
    return a + b + c

print(kw_only(1, b=2, c=3))
# print(kw_only(1, 2, 3))  # TypeError β€” b and c are keyword-only

# Example 21: combined positional-only and keyword-only
def combined(a, b, /, c, *, d, e):
    return a + b + c + d + e

print(combined(1, 2, 3, d=4, e=5))
πŸ” Why this code is used: Positional-only and keyword-only parameters give you fine-grained control over how functions can be called β€” they're useful for API design and preventing ambiguity.

βš™οΈ How it operates: Parameters before / are positional-only β€” they cannot be passed as keywords. Parameters after * are keyword-only β€” they cannot be passed positionally.

πŸ“Š How it affects the program: Example 19 shows that a and b are positional-only β€” calling pos_only(a=1, b=2, c=3) raises TypeError.

🎯 How it stimulates the outcome: Positional-only parameters are used in the Python standard library to maintain backward compatibility β€” they allow parameter names to be changed without breaking existing code.

πŸ“Œ Significance: These syntax features give API designers precise control over function interfaces β€” they're a sign of a mature, carefully designed language.

🌐 Variable Scope β€” The LEGB Rule

Understanding where variables are defined and accessed

🌐 LEGB Rule

Local, Enclosing, Global, Built-in

Python resolves variable names in a specific order: Local β†’ Enclosing β†’ Global β†’ Built-in. Understanding this order prevents confusing UnboundLocalError bugs.

# Example 22: local scope
def local_example():
    x = 10  # local variable
    print(x)

local_example()
# print(x)  # NameError β€” x is not defined globally

# Example 23: global scope
y = 20  # global variable

def global_example():
    print(y)  # reads the global variable

global_example()

# Example 24: modifying a global variable with 'global'
z = 30

def modify_global():
    global z
    z = 40

modify_global()
print(z)

# Example 25: enclosing scope (closure)
def outer():
    outer_var = "outer"

    def inner():
        print(outer_var)  # accesses the enclosing scope

    inner()

outer()

# Example 26: modifying an enclosing variable with 'nonlocal'
def counter():
    count = 0

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

    return increment

inc = counter()
print(inc())
print(inc())
πŸ” Why this code is used: Scope determines where variables are visible β€” understanding it prevents a whole category of bugs.

βš™οΈ How it operates: Python looks up variable names in this order: Local (inside the current function), Enclosing (in outer functions), Global (at the module level), Built-in (Python's built-in names). The global keyword lets you modify a global variable from inside a function. The nonlocal keyword lets you modify a variable in an enclosing (but not global) scope.

πŸ“Š How it affects the program: Example 22 shows that variables defined inside a function are not visible outside it. Example 24 shows that you need global to modify a global variable.

🎯 How it stimulates the outcome: The nonlocal pattern in Example 26 is used to create function factories and closures β€” a powerful functional programming technique.

πŸ“Œ Significance: The LEGB rule is one of the most important concepts in Python β€” it explains why variables behave the way they do in different contexts.

Ξ» Lambda Functions β€” Anonymous Functions

Concise, single-expression functions for callbacks and functional programming

Ξ» Lambda Basics

lambda β€” Functions Without a Name

A lambda function is a small, anonymous function defined with the lambda keyword. It can have any number of parameters but only one expression β€” the result of which is returned automatically.

# Example 27: a simple lambda
square = lambda x: x ** 2
print(square(5))

# Example 28: lambda with multiple parameters
add = lambda a, b: a + b
print(add(3, 5))

# Example 29: lambda used with map()
numbers = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, numbers))
print(doubled)

# Example 30: lambda used with filter()
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)

# Example 31: lambda used with sorted() key
people = [{"name": "Bob", "age": 30}, {"name": "Ana", "age": 25}]
sorted_by_age = sorted(people, key=lambda p: p["age"])
print(sorted_by_age)
πŸ” Why this code is used: Lambdas are ideal for short, single-use functions β€” especially as callbacks for map(), filter(), sorted(), and similar functions.

βš™οΈ How it operates: lambda args: expression creates a function object. The expression is evaluated and returned when the lambda is called. Lambdas are restricted to a single expression β€” they cannot contain statements or multiple lines.

πŸ“Š How it affects the program: Example 29 shows that map() with a lambda is more concise than defining a separate function for doubling numbers.

🎯 How it stimulates the outcome: The sorted() example (31) is a common real-world use β€” sorting a list of dictionaries by a specific key using a lambda.

πŸ“Œ Significance: Lambdas are a key feature of functional programming in Python β€” they enable a more concise, expressive style for certain operations.

πŸ”„ Recursion β€” Functions That Call Themselves

Elegant problem-solving for naturally recursive problems

πŸ”„ Recursion Basics

Base Cases and Recursive Cases

A recursive function is a function that calls itself. Every recursive function must have a base case that stops the recursion, and a recursive case that calls itself with a smaller or simpler input.

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

print(factorial(5))

# Example 33: Fibonacci with recursion
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))

# Example 34: recursion with memoization for performance
def fib_memo(n, memo={}):
    if n <= 1:
        return n
    if n not in memo:
        memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
    return memo[n]

print(fib_memo(50))

# Example 35: recursion depth limit
def deep(n):
    if n == 0:
        return "done"
    return deep(n - 1)

# print(deep(10000))  # RecursionError: maximum recursion depth exceeded
print(deep(1000))  # works
πŸ” Why this code is used: Recursion is the natural way to solve problems that have a recursive structure β€” tree traversal, factorial, Fibonacci, and many mathematical and algorithmic problems.

βš™οΈ How it operates: Each recursive call creates a new stack frame. The base case stops the recursion and begins returning values back up the call stack. Python has a default recursion limit of 1000 β€” deeper recursion raises RecursionError.

πŸ“Š How it affects the program: Example 34 shows memoization β€” caching results to avoid exponential time complexity. Without memoization, fibonacci(50) would take an impossibly long time.

🎯 How it stimulates the outcome: Recursion is elegant but can be memory-intensive β€” for very deep recursion, an iterative solution is often better.

πŸ“Œ Significance: Recursion is a fundamental technique in computer science β€” mastering it opens up a whole new way of thinking about problems.

✨ Decorators β€” Wrapping and Extending Functions

One of Python's most powerful and elegant features

✨ Decorator Basics

Functions that Modify Other Functions

A decorator is a function that takes another function as an argument and returns a new function that wraps the original. This is a powerful pattern for adding behaviour (logging, timing, authentication, caching) without modifying the original function's code.

# Example 36: a simple decorator
def uppercase_decorator(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@uppercase_decorator
def greet(name):
    return f"Hello, {name}"

print(greet("Glenn"))

# Example 37: a timing decorator
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} took {end - start:.6f}s")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(0.1)
    return "done"

slow_function()

# Example 38: decorators with arguments (factory)
def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def say_hello():
    print("Hello!")

say_hello()

# Example 39: preserving metadata with functools.wraps
import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log
def add(a, b):
    """Add two numbers."""
    return a + b

print(add.__name__)
print(add.__doc__)
πŸ” Why this code is used: Decorators are used extensively in frameworks (Flask routes, Django views, pytest fixtures) and for cross-cutting concerns like logging, timing, authentication, and caching.

βš™οΈ How it operates: @decorator is syntactic sugar for func = decorator(func). The decorator function takes func as an argument and returns a new function that wraps it. Decorator factories (Example 38) are functions that return decorators, allowing parameters to be passed to the decorator.

πŸ“Š How it affects the program: Without functools.wraps (Example 39), the wrapper function would lose the original function's name and docstring β€” wraps preserves this metadata.

🎯 How it stimulates the outcome: Example 37's timer decorator is a real-world pattern β€” it adds timing behaviour to any function without modifying its code.

πŸ“Œ Significance: Decorators are one of Python's most distinctive and powerful features β€” they enable clean, reusable behaviour modification across your codebase.

πŸ’Ό Real-World Mini Projects

Three self-contained programs that combine everything above

⏱️ Project 1 – Function Timer Decorator

Measuring Function Performance with a Decorator

Demonstrates decorator creation, *args/**kwargs, and timing β€” a practical tool for performance profiling.

import time
import functools

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} took {end - start:.6f}s")
        return result
    return wrapper

@timer
def compute(n):
    return sum(i ** 2 for i in range(n))

print("Result:", compute(1000000))
πŸ” Why this code is used: Timing functions is a common need in performance optimisation β€” this decorator makes it reusable.

βš™οΈ How it operates: The timer decorator wraps any function, measuring and printing the elapsed time. functools.wraps preserves the original function's metadata.

πŸ“Š How it affects the program: The decorator can be applied to any function with a simple @timer annotation β€” no code duplication.

🎯 How it stimulates the outcome: This pattern scales to real performance profiling β€” you can time specific operations without cluttering your business logic.

πŸ“Œ Significance: This is a real-world use of decorators β€” adding cross-cutting behaviour cleanly and reusably.
πŸ“ Project 2 – Input Validation with *args and **kwargs

Flexible Input Validation with Variable-Length Arguments

def validate_user(required_fields, *args, **kwargs):
    """Validate that all required fields are present in kwargs."""
    missing = [field for field in required_fields if field not in kwargs]
    if missing:
        raise ValueError(f"Missing required fields: {missing}")
    if args:
        print(f"Extra positional arguments ignored: {args}")
    return {field: kwargs[field] for field in required_fields}

# Example 40: valid call
result = validate_user(["name", "email"], name="Glenn", email="g@example.com")
print(result)

# Example 41: missing field
try:
    result = validate_user(["name", "email"], name="Glenn")
except ValueError as e:
    print(f"Error: {e}")
πŸ” Why this code is used: Validation of function inputs is a common need β€” this pattern shows how to validate keyword arguments flexibly.

βš™οΈ How it operates: The function takes a list of required fields and uses **kwargs to collect all keyword arguments. It checks which required fields are missing and raises an error.

πŸ“Š How it affects the program: This pattern is useful for API endpoints, configuration validation, and any function that receives structured data.

🎯 How it stimulates the outcome: The function ignores extra positional arguments (collected in *args) β€” this is a clean way to handle unexpected input.

πŸ“Œ Significance: This pattern demonstrates how *args and **kwargs enable flexible, robust function interfaces.
πŸ” Project 3 – Memoization with a Decorator

Caching Expensive Function Results

import functools

def memoize(func):
    cache = {}
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        key = (args, tuple(kwargs.items()))
        if key not in cache:
            cache[key] = func(*args, **kwargs)
        return cache[key]
    return wrapper

@memoize
def expensive(n):
    """A computationally expensive function."""
    print(f"Computing expensive({n})...")
    return sum(i ** 2 for i in range(n))

print(expensive(1000))
print(expensive(1000))  # cached β€” no computation
πŸ” Why this code is used: Memoization caches the results of expensive function calls β€” a classic optimisation technique.

βš™οΈ How it operates: The memoize decorator stores results in a dictionary keyed by the function arguments. If the same arguments are passed again, the cached result is returned immediately.

πŸ“Š How it affects the program: The expensive function is only called once for each unique set of arguments β€” subsequent calls are instant.

🎯 How it stimulates the outcome: Python's built-in functools.lru_cache provides this functionality β€” this example shows how it works under the hood.

πŸ“Œ Significance: Memoization is one of the most common uses of decorators in real-world Python code.

❓ Frequently Asked Questions

Common questions about Python functions, answered

What is the difference between a parameter and an argument?

+

A parameter is a variable in the function definition β€” it's a placeholder for the value that will be passed. An argument is the actual value that is passed to the function when it's called. In def greet(name):, name is a parameter. In greet("Glenn"), "Glenn" is an argument.

What does return do in a function?

+

The return statement exits the function and sends a value back to the caller. If no return is used, the function returns None. A function can have multiple return statements (e.g., for early exits), but only one is executed per call.

What is *args and **kwargs?

+

*args collects extra positional arguments into a tuple. **kwargs collects extra keyword arguments into a dictionary. They make functions flexible β€” able to accept any number of arguments. The names args and kwargs are conventions, not keywords.

What is the LEGB rule?

+

LEGB stands for Local, Enclosing, Global, Built-in β€” it's the order in which Python looks up variable names. Python checks the local scope first, then any enclosing (outer) scopes, then the global scope, and finally the built-in scope.

What is a lambda function?

+

A lambda is a small, anonymous function defined with lambda. It can have any number of parameters but only one expression. Lambdas are ideal for short, single-use callbacks in map(), filter(), and sorted().

What is recursion in Python?

+

Recursion is when a function calls itself. It's useful for problems with a recursive structure (tree traversal, factorial, Fibonacci). Every recursive function needs a base case to stop recursion and a recursive case that moves toward the base case.

What is a decorator and why would I use one?

+

A decorator is a function that wraps another function, adding behaviour without modifying its code. Use decorators for cross-cutting concerns like logging, timing, authentication, and caching. They're a key feature in frameworks like Flask, Django, and pytest.

What is the difference between global and nonlocal?

+

global allows you to modify a variable at the module (global) level from inside a function. nonlocal allows you to modify a variable in an enclosing (but not global) scope β€” used in nested functions and closures.

What is a docstring?

+

A docstring is a string literal that appears as the first statement in a function, class, or module. It's used for documentation and is accessible via __doc__ and help(). Writing docstrings is a professional practice.

Can a function return multiple values?

+

Yes β€” a function can return multiple values as a tuple. The caller can unpack the tuple into separate variables: a, b = func(). This is a common Python pattern.

✏️ Practice Exercises & Quizzes

Test your understanding with hands-on problems and multiple-choice questions

πŸ§ͺ Exercise 1 – Write a Function to Calculate the Average

Task: Write a function that takes a list of numbers and returns the average.

Example: average([1, 2, 3, 4, 5]) β†’ 3.0

def average(numbers: list) -> float:
    # Your code here
    pass

# Test
print(average([1, 2, 3, 4, 5]))  # 3.0
def average(numbers: list) -> float:
    if not numbers:
        return 0.0
    return sum(numbers) / len(numbers)

# Explanation:
# 1. Check for an empty list to avoid division by zero
# 2. Use sum() and len() for a clean, readable implementation
πŸ§ͺ Exercise 2 – Write a Function Using *args

Task: Write a function that takes any number of arguments and returns the product of all non-zero arguments.

Example: product(2, 3, 4, 0, 5) β†’ 120

def product(*args) -> int:
    # Your code here
    pass

# Test
print(product(2, 3, 4, 0, 5))  # 120
def product(*args) -> int:
    result = 1
    for n in args:
        if n != 0:
            result *= n
    return result

# Explanation:
# 1. *args collects all positional arguments into a tuple
# 2. Iterate over args, multiplying only non-zero values
# 3. Return the final product
πŸ§ͺ Exercise 3 – Write a Recursive Function

Task: Write a recursive function that computes the sum of all integers from 1 to n.

Example: sum_to_n(5) β†’ 15

def sum_to_n(n: int) -> int:
    # Your code here
    pass

# Test
print(sum_to_n(5))  # 15
def sum_to_n(n: int) -> int:
    if n <= 0:
        return 0
    return n + sum_to_n(n - 1)

# Explanation:
# 1. Base case: n <= 0 returns 0
# 2. Recursive case: n + sum_to_n(n - 1)
# 3. This computes n + (n-1) + (n-2) + ... + 1
πŸ§ͺ Exercise 4 – Write a Decorator

Task: Write a decorator that prints "Function called!" before every call.

def log_call(func):
    # Your decorator code here
    pass

@log_call
def say_hello(name):
    return f"Hello, {name}!"

print(say_hello("Glenn"))
def log_call(func):
    def wrapper(*args, **kwargs):
        print("Function called!")
        return func(*args, **kwargs)
    return wrapper

# Explanation:
# 1. The decorator takes a function as an argument
# 2. It defines a wrapper function that prints the message
# 3. The wrapper calls the original function and returns its result
πŸ§ͺ Exercise 5 – Write a Function with Default and Keyword-Only Parameters

Task: Write a function format_text that takes a string and optional parameters uppercase (default False) and prefix (default ""), where both optional parameters must be passed as keywords.

Example: format_text("hello", uppercase=True, prefix=">> ") β†’ >> HELLO

def format_text(text, *, uppercase=False, prefix=""):
    # Your code here
    pass

# Test
print(format_text("hello", uppercase=True, prefix=">> "))  # >> HELLO
def format_text(text, *, uppercase=False, prefix=""):
    result = text
    if uppercase:
        result = result.upper()
    return prefix + result

# Explanation:
# 1. The * makes uppercase and prefix keyword-only
# 2. They have default values, so they're optional
# 3. The function formats the text based on the parameters
πŸ“ Quick Quiz

Multiple-Choice Questions

1. What keyword is used to define a function in Python?

A) function
B) def
C) define
D) func

2. What does *args do in a function definition?

A) Collects extra keyword arguments
B) Collects extra positional arguments
C) Defines a default argument
D) Raises a syntax error

3. What does a function return if there is no return statement?

A) 0
B) None
C) False
D) An empty string

4. What is the LEGB rule about?

A) Function call order
B) Variable name lookup order
C) Import order
D) Loop iteration order

5. Which keyword is used to modify a variable in an enclosing scope?

A) global
B) nonlocal
C) enclosing
D) outer

6. What is a lambda function?

A) A named function
B) An anonymous function
C) A recursive function
D) A decorator

7. What does functools.wraps do in a decorator?

A) Preserves function metadata
B) Wraps the function in a try/except
C) Makes the function faster
D) Converts arguments to strings

8. What is the default recursion limit in Python?

A) 100
B) 1000
C) 10000
D) There is no limit

🏁 Congratulations – You've Mastered Python Functions!

You've now covered the fundamental building block of Python programming β€” the function β€” in genuine depth, from basic definition to advanced decorator patterns.

βœ… What You've Accomplished

  • Understood function definition, calls, and the return statement
  • Mastered positional, keyword, and default parameters
  • Used *args and **kwargs for flexible function interfaces
  • Learned the LEGB scope rule β€” Local, Enclosing, Global, Built-in
  • Wrote lambda functions for concise callbacks
  • Explored recursion β€” functions that call themselves
  • Created decorators to wrap and extend functions
  • Used docstrings and type annotations for self-documenting code
  • Built three real-world mini projects: a timer decorator, input validation, and memoization
  • Worked through 50+ runnable examples, five exercises, and eight quiz questions

🌍 Why This Matters Beyond This Page

Everything you learned here reappears constantly across Python programming. Every program is built from functions. Every framework β€” Flask, Django, pytest β€” is built on decorators, callbacks, and function composition. The patterns you've practiced β€” *args/**kwargs, lambda functions, recursion, decorators β€” are used in production code every day. Functions are not a beginner topic you leave behind; they are the foundation of every program you'll ever write.

πŸ“š Next Steps

Now that functions feel natural, consider extending into:

  • Classes and OOP β€” functions are the building blocks of methods
  • Modules and Packages β€” organising functions into reusable libraries
  • Functional Programming β€” map, filter, reduce, and function composition
  • Generators β€” functions that yield values lazily
  • Context Managers β€” functions with __enter__ and __exit__

πŸ’Œ A Final Word

Functions are where Python's philosophy of "readable, explicit code" meets practical problem-solving. calculate_tax(amount) tells a reader exactly what the code does in a way that inline tax logic never can. The gap between "I can write def f():" and "I understand scope, *args/**kwargs, and decorators" is the same gap that separates a beginner from a confident intermediate developer. You've just closed a large part of that gap for one of Python's most fundamental concepts. Keep experimenting, keep breaking things intentionally to see why they break, and keep building.

Now go build something reusable and elegant with your functions! πŸ› οΈ

Poetic Bytes · 2026 · Made with ❀️ by Glenn Junsay Pansensoy

"A function is a promise: give me the right inputs, and I'll give you the right output, every time."