<< Home

Chapter 8: Functions and Functional Features

Functions are the building blocks of structure and reuse in Python. They let you group statements into named units, turning repeated logic into single, clear definitions. A good function captures one idea, performs one task, and hides unnecessary detail behind a clean interface. This modularity is what turns a script into a program and a collection of programs into a coherent system.

In Python, functions are not only organisational tools but also objects in their own right. They can be passed as arguments, returned from other functions, stored in data structures, and created dynamically. This flexibility comes from Python’s functional heritage, where functions are treated as first-class citizens—a concept that opens up powerful and expressive programming patterns.

⚠️ In Python, everything that can be named can also be passed around. Functions are no exception; they are objects, just like lists, strings, or classes. This simple rule is the key to understanding Python’s functional style.

This chapter explores how functions work in Python and how they connect to the broader functional features of the language. We’ll cover definition and calling syntax, parameters and return values, scope and closures, anonymous functions with lambda, higher-order functions, and the role of decorators in extending behaviour. Along the way, you’ll see how Python’s treatment of functions as objects allows for patterns that are both elegant and deeply practical.

Defining and calling functions

Every function in Python begins with the def keyword, followed by the function’s name, a list of parameters in parentheses, and a colon. The body of the function is indented and contains the statements that define what the function does. When the function is called, Python executes those statements and optionally returns a value with the return statement.

def greet(name):
  print("Hello,", name)

Here, greet is a function that takes a single argument, name. When called, it prints a greeting. The function itself does nothing until invoked by name:

greet("Ada")
greet("Grace")

Function names follow the same rules as variables: they must begin with a letter or underscore, contain only letters, digits, and underscores, and are case-sensitive. By convention, Python functions use lowercase words separated by underscores (snake_case).

Returning values

Functions often compute and return a result. The return statement ends the function and passes a value back to the caller. If no return is given, Python automatically returns None.

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

result = add(5, 3)
print(result) # 8

A function can return any Python object, including lists, tuples, dictionaries, or even other functions. Returning multiple values is simply done by separating them with commas, which creates a tuple implicitly.

def divide(x, y):
  quotient = x // y
  remainder = x % y
  return quotient, remainder

q, r = divide(10, 3)
print(q, r) # 3 1

Default and optional arguments

Parameters can have default values, making them optional when calling the function. Defaults are evaluated once when the function is defined, not each time it is called.

def greet(name, greeting="Hello"):
  print(greeting, name)

greet("Ada")
greet("Grace", "Hi")

Defaults can be of any type, but mutable defaults (like lists or dictionaries) should be avoided because they persist across calls. Use None as a safe default if you need to create a new object each time.

def append_item(item, lst=None):
  if lst is None:
    lst = []
  lst.append(item)
  return lst

Keyword and positional arguments

Arguments in Python can be passed by position or by keyword. Positional arguments match parameters in order, while keyword arguments specify them explicitly. You can mix both styles as long as positional arguments come first.

def describe(person, age, city):
  print(f"{person} is {age} and lives in {city}.")

describe("Ada", 36, "London")
describe(person="Grace", age=35, city="New York")
describe("Linus", city="Helsinki", age=33)

This flexibility makes code easier to read and maintain. It also helps prevent errors when a function takes several parameters of similar types.

Docstrings and function help

A docstring (short for documentation string) is a string literal placed immediately after a function definition. It describes what the function does, its parameters, and its return value. Python stores this text as the function’s .__doc__ attribute and displays it when using the help() function.

def square(n):
  """Return the square of n."""
  return n * n

help(square)

Clear docstrings turn your functions into self-documenting components, making code easier to use, share, and maintain. Well-written docstrings follow the same conventions as those in the standard library: a short summary on the first line, optionally followed by more detail or examples.

💡 Think of a function as a contract: parameters are the input, return values are the output, and the docstring is the agreement that explains what happens in between.

By defining and calling functions clearly, you give structure to your code, make behaviour reusable, and create natural building blocks for larger systems. Every good Python program grows from small, well-defined functions that each do one thing well.

Positional, keyword, and default parameters

Python gives you several ways to pass data into functions, allowing for a mix of flexibility and clarity. Function parameters can be positional, keyword-based, or have default values, and all three styles can coexist in a single definition. Understanding how these work together is key to writing functions that are both powerful and easy to call.

Positional parameters

By default, arguments in a function call are matched to parameters by their order. These are known as positional parameters. The first argument corresponds to the first parameter, the second to the second, and so on.

def move(x, y):
  print(f"Moving to coordinates ({x}, {y})")

move(10, 20)

Positional arguments are compact and familiar, but can become confusing when there are many parameters or when the order is easy to forget. This is where keyword arguments improve readability.

Keyword arguments

A keyword argument specifies which parameter a value is meant for, regardless of order. You write the parameter name followed by an equals sign and the value. Keyword arguments make function calls self-documenting, which is particularly useful when functions have many optional or similarly typed parameters.

def connect(host, port, secure):
  print(f"Connecting to {host}:{port} (secure={secure})")

connect("example.com", 443, True)                   # positional
connect(port=443, host="example.com", secure=True)  # keyword

When combining positional and keyword arguments, positional ones must always come first. This rule avoids ambiguity and keeps function calls consistent.

connect("example.com", port=443, secure=True)  # valid
connect(port=443, "example.com", secure=True)  # invalid

Default parameters

Parameters can be given default values, which makes them optional when calling the function. If the caller omits a value, Python uses the default instead.

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

greet("Ada")
greet("Grace", "Hi")

Defaults are evaluated once at the time the function is defined, not every time it runs. This means that mutable default values like lists or dictionaries persist across calls, which can lead to subtle bugs.

def add_item(item, collection=[]):
  collection.append(item)
  return collection

print(add_item("apple"))
print(add_item("banana"))  # reuses the same list

The second call here adds to the same list as the first, since the default list is shared. To avoid this, use None as a placeholder and create a new list inside the function.

def add_item(item, collection=None):
  if collection is None:
    collection = []
  collection.append(item)
  return collection

Combining styles

Functions often use a mix of positional, keyword, and default parameters to balance flexibility with clarity. For instance, positional parameters might define essential inputs, while keyword and default parameters handle optional behaviour.

def send_message(to, subject, body="", urgent=False):
  print(f"To: {to}")
  print(f"Subject: {subject}")
  if urgent:
    print("⚠️ URGENT")
  print(body)

send_message("team@example.com", "Meeting", urgent=True)

This design lets the caller specify what matters most, while relying on defaults for the rest. It’s a clear, Pythonic way to make functions flexible without making them hard to read.

💡 When designing function interfaces, make essential parameters positional and simple, but make optional ones keyword-based. This keeps code readable and reduces the risk of errors when calling functions with many arguments.

Variable-length arguments

Sometimes you don’t know in advance how many arguments a function should accept. Python solves this with variable-length arguments, which allow a function to receive an arbitrary number of positional or keyword parameters. This makes functions highly flexible and adaptable to different calling patterns.

*args: variable positional arguments

When you prefix a parameter with an asterisk (*), Python collects any extra positional arguments into a tuple. This allows the function to handle any number of values without needing to define them individually.

def total(*numbers):
  print(numbers)
  return sum(numbers)

print(total(2, 4, 6))
print(total(1, 3, 5, 7, 9))

Here, all supplied arguments are packed into the tuple numbers. The function can then iterate over them as needed. This technique is especially useful when building wrapper functions or utilities that forward arguments to other functions.

**kwargs: variable keyword arguments

Similarly, prefixing a parameter with two asterisks (**) collects any additional keyword arguments into a dictionary. This lets you capture named arguments that were not explicitly defined in the function’s signature.

def describe(**info):
  for key, value in info.items():
    print(f"{key}: {value}")

describe(name="Ada", job="Engineer", city="London")

Inside the function, info is a dictionary containing all the keyword arguments passed in. This pattern is common when functions need to accept flexible configuration data or pass arbitrary options along to another function.

Combining fixed and variable parameters

You can mix normal parameters with *args and **kwargs in one definition. The general order is: