› Module 5: Functions › 5.4 Recursion
M O D U L E

Module 5: Functions

Packaging reusable logic into named, callable blocks — and the unusually flexible ways Python lets you pass arguments to them.

5.1 Defining and Calling Functions

A function is defined with the def keyword, a name, a parenthesized list of parameters, and an indented body. It hands a result back to whoever called it via return — and if a function never reaches a return statement, Python quietly returns None for it instead of raising an error.

Because Python has no type declarations on parameters, a single function definition can happily accept arguments of completely different types, as long as whatever operations happen inside the function body make sense for whatever was actually passed in.

One Function, Many Types (Polymorphism via Duck Typing)

# ONE FUNCTION, MANY TYPES
def add(a, b):
    return a + b

print(add(2, 3))         # 5
print(add("Py", "thon")) # "Python" — works for strings too!
print(add([1, 2], [3]))  # [1, 2, 3] — works for lists too!

Variable Scope: The LEGB Rule

When resolving a variable name inside a function, Python searches four concentric scope namespaces in strict order (LEGB):

  1. L - Local: Names assigned inside the active function.
  2. E - Enclosing: Names in the local scope of any outer enclosing functions (closures).
  3. G - Global: Names assigned at the top level of the module file.
  4. B - Built-in: Pre-loaded names in Python (e.g. len, range, print).

To modify a global or enclosing variable from inside a local function, you must explicitly declare it with global x or nonlocal x.

5.2 Default, *args, and **kwargs Parameters

Python's function signatures are considerably more flexible than a fixed C-style parameter list:

  • A parameter can carry a default value (def greet(name, age=18):), which makes it optional for the caller to supply.
  • Beyond that, *args scoops up any number of extra positional arguments into a tuple, letting a function accept a variable, unplanned-for number of inputs.
  • **kwargs does the same job for extra keyword arguments, collecting them into a dictionary.

Used together, these let a single function signature gracefully handle almost any calling pattern.

def greet(name, age=18, *args, **kwargs): name (positional) age=18 (default value) *args (extra positionals as a tuple) **kwargs (extra keywords as a dict) Positional args are matched by order; defaults fill in when omitted; *args and **kwargs soak up whatever is left over.
Figure 5.1 — How positional, default, *args, and **kwargs parameters divide up a function call.
The Infamous Mutable Default Argument Trap:

Default parameter values are evaluated only once when the function is defined, not each time it is called. If you use a mutable object (like a list) as a default argument, all calls share the exact same list:

# BUGGY:
def append_to(val, target=[]): # target is shared across calls!
    target.append(val)
    return target

print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] — unexpectedly accumulated!

# THE PYTHONIC FIX:
def append_to_safe(val, target=None):
    if target is None:
        target = []
    target.append(val)
    return target

5.3 Lambda (Anonymous) Functions

A lambda is a small, unnamed function limited to a single expression: lambda x, y: x + y.

Lambdas are almost never used as a substitute for a full def when the logic is more than one line — they shine specifically as short, throwaway, inline arguments passed to another function, most commonly the key= parameter of sorted(), or inside filter()/map() calls.

A Lambda as a Sort Key (from the Guide)

# A LAMBDA AS A SORT KEY
nums = [5, 2, 8, 1]
nums.sort(key=lambda x: -x) # sort descending using a lambda
print(nums) # [8, 5, 2, 1]

# Sorting complex dictionaries:
students = [
    {"name": "Asha", "score": 92},
    {"name": "Rahul", "score": 78},
    {"name": "Pooja", "score": 95}
]
students.sort(key=lambda s: s["score"], reverse=True)

5.4 Recursion

A Python function is free to call itself, exactly as in C, provided it always includes a base case — a condition that eventually stops the chain of self-calls. Without one, the function would call itself forever.

As a safety net against this, Python enforces a default maximum recursion depth (typically 1000 calls deep), which can be raised with sys.setrecursionlimit() if a program genuinely needs deeper recursion.

Factorial by Recursion

# FACTORIAL BY RECURSION
def fact(n):
    if n == 0:
        return 1
    return n * fact(n - 1)

print(fact(4)) # 24
fact(4) fact(3) fact(2) fact(1) return 1 (base case) fact(1) = 1 fact(2) = 2 * 1 = 2 fact(3) = 3 * 2 = 6 → fact(4) = 4 * 6 = 24
Figure 5.2 — fact(4) expands downward until the base case, then multiplies its way back upward.
MODULE COMPREHENSIVE QUIZ

Module 5 Final Quiz

Test your understanding across all 4 topics of this module with interview-grade questions, tricky edge cases, and code output predictions.

Take Module 5 Final Quiz →
Yukti Mascot