Chapter 4

Functions

4.0 Intro · 4.1 Functions · 4.2 Function Design · 4.3 Recursion

← → or Space to navigate · F for fullscreen

4.1 Functions

Defining, calling, parameters, return values, type annotations, flexible arguments

Defining & Calling Functions

def greet(name):
    """Return a greeting string."""
    return f"Hello, {name}!"

print(greet("Alice"))   # Hello, Alice!
  • def keyword defines a function.
  • The body is indented.
  • return sends a value back to the caller.
  • Without return, a function returns None.

Parameters & Arguments

def power(base, exp=2):     # default value
    return base ** exp

print(power(3))      # 9   (exp=2)
print(power(3, 3))   # 27  (exp=3)
print(power(exp=3, base=2)) # 8 (keyword)
Term Meaning
Parameter Variable in the function definition
Argument Value passed when calling

Flexible Arguments: *args and **kwargs

# *args — variable positional arguments (tuple)
def total(*args):
    return sum(args)

print(total(1, 2, 3, 4))   # 10

# **kwargs — variable keyword arguments (dict)
def describe(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

describe(name="Alice", age=25, city="Rolla")

Full parameter order

def f(pos, /, normal, *, kw_only,
      **kwargs):
    ...
Type Example
Positional f(1)
Default f(x=2)
*args Collects extras → tuple
**kwargs Collects key=val → dict

4.2 Function Design

Scope, pure functions, composition, docstrings, lambda

Scope

  • Local scope: variables inside a function.
  • Global scope: variables at the module level.
  • A function can read globals but not write to them without global.
x = 10   # global

def show():
    print(x)   # reads global — OK

def double():
    global x
    x *= 2     # modifies global — needs 'global'

Prefer returning values over modifying globals. Functions that depend only on their arguments are easier to test.

Function composition

def celsius_to_fahrenheit(c):
    return c * 9/5 + 32

def format_temp(c):
    f = celsius_to_fahrenheit(c)
    return f"{c}°C = {f:.1f}°F"

print(format_temp(100))   # 100°C = 212.0°F

Pure Functions & Composition

def add_bonus(score, bonus):
    return score + bonus

def mean(values):
    return sum(values) / len(values)

def letter_grade(score):
    if score >= 90:
        return "A"
    if score >= 80:
        return "B"
    return "Needs review"
  • Pure functions depend only on their inputs.
  • Side effects change outside state.
  • Small functions are easier to test.
  • Composition combines small functions into a larger workflow.

Prefer returning new values over mutating shared state unless mutation is the point of the function.

Docstrings & Lambda

Docstrings

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

    Args:
        radius: The radius (non-negative float).
    Returns:
        Area as a float.
    """
    import math
    return math.pi * radius ** 2

Access with help(area) or area.__doc__.

Lambda — small anonymous function

# Named function
def square(x): return x ** 2

# Equivalent lambda
square = lambda x: x ** 2

# Common use: sort key
words = ["banana", "kiwi", "apple"]
words.sort(key=lambda w: len(w))
print(words)   # ['kiwi', 'apple', 'banana']

Use def when the logic needs a name or more than one expression.

Closures

A nested function can read variables from its enclosing scope. When the outer function returns the inner one, the inner function remembers those variables: it is a closure.

A function factory returns configured closures. Use nonlocal to update an enclosing variable.

Decorators in Chapter 10 are built on closures.

def make_discount(rate):
    return lambda price: round(price * (1 - rate), 2)

student_price = make_discount(0.10)
print(student_price(40))   # 36.0

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

click = make_counter()
print(click(), click(), click())   # 1 2 3

4.3 Recursion

Recursive functions · base cases · tracing calls · memoization

Recursion

A function that calls itself. Every recursive function needs:

  1. A base case — stops the recursion.
  2. A recursive case — moves toward the base case.

Without a base case, recursion runs forever → RecursionError.

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

print(factorial(5))   # 120
# 5 * 4 * 3 * 2 * 1 * 1 = 120
def fibonacci(n):
    if n <= 1:          # base case
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(10))  # 55

Recursion on Lists

Split a list into its first item, items[0], and the rest, items[1:].

  • Base case: an empty list; not items is True.
  • Recursive case: handle the first item, then recurse on the shorter rest.
count_items(['apple', 'banana', 'cherry'])
= 1 + count_items(['banana', 'cherry'])
= 1 + (1 + count_items(['cherry']))
= 1 + (1 + (1 + count_items([])))
= 1 + (1 + (1 + 0)) = 3
def count_items(items):
    if not items:              # base case
        return 0
    return 1 + count_items(items[1:])

fruits = ['apple', 'banana', 'cherry']
print(count_items(fruits))   # 3

Chapter 4 — Quick Reference

Concept Key syntax / notes
Define function def name(params): return value
Default param def f(x=10):
*args Collect extra positional → tuple
**kwargs Collect extra keyword → dict
Docstring Triple-quoted string as first statement
Lambda lambda args: expression
Scope Local vars stay local; use return
Pure function Same inputs produce same output; no outside changes
Recursion Must have a base case
Memoization Store repeated recursive results

End of Chapter 4

Next: Chapter 5 — Exceptions & Testing

try · except · raise · pytest · unittest · doctest