Python allows you to define your own functions, in addition to built-in ones like print() and len(). This improves code organization, reduces repetition, and improves readability. A function can be called as many times as needed, from anywhere later in the same script.

The Python documentation has a guide on Defining Functions.

Purpose of Functions

Functions are reusable blocks of code that accept inputs, perform operations, and return outputs. They are especially useful when a calculation needs to be performed multiple times with different inputs. They can also abstract the details of a complex operation into a simpler interface. For example, when you call math.sqrt(x) you are not particularly interested in the algorithm used to compute a square root. You just want the answer.

Syntax

A function is defined with the def keyword:

def function_name(param1, param2):
    # code to compute a result
    return value

Calling function_name() does nothing until you actually invoke it by name, followed by parentheses:

def greet():
    print("Hello from a function!")

greet()

Hello from a function!

Parameters and Return Values

A function can accept one or more parameters, use them in its body, and send a result back with return. Execution of the function stops as soon as return runs.

def square(x):
    return x ** 2

result = square(5)
print(result)
print(square(3) + square(4))

25
25

Example: Axial Stress

Question

A force of 5000 N is applied to a beam with a cross-sectional area of 0.002 m2. Given the formula for axial stress:

\[\sigma = \frac{F}{A}\]

write a Python function that calculates stress for any force and area, then use it to find the stress for the givens above. Express your answer in MPa.

Solution

def axial_stress(force, area):
    """Return axial stress (Pa) given a force (N) and cross-sectional area (m^2)."""
    return force / area

sigma = axial_stress(5000, 0.002)
print(f"{sigma / 1e6:.2f} MPa")

2.50 MPa

Default Parameter Values

A parameter can be given a default value, used whenever the caller doesn’t provide one. Defaults make a function flexible without forcing every caller to specify every input:

def time_to_deplete(charge_pct, drain_rate_pct=5.0):
    """Hours until charge_pct reaches zero, at a constant drain rate."""
    return charge_pct / drain_rate_pct

print(time_to_deplete(80))                        # uses the default rate
print(time_to_deplete(80, drain_rate_pct=8.0))     # override by keyword
print(time_to_deplete(80, 2.5))                    # override by position

16.0
10.0
32.0

Keyword vs. Positional Arguments

Arguments can be passed by position, matched to parameters in order, or by keyword, matched by name regardless of order:

def describe_stage(name, mass_kg, reusable=False):
    print(f"{name}: {mass_kg} kg, reusable={reusable}")

describe_stage("Booster", 25000)                              # positional
describe_stage("Booster", 25000, True)                        # positional
describe_stage(name="Booster", mass_kg=25000, reusable=True)  # keyword
describe_stage("Booster", reusable=True, mass_kg=25000)       # mixed

Booster: 25000 kg, reusable=False
Booster: 25000 kg, reusable=True
Booster: 25000 kg, reusable=True
Booster: 25000 kg, reusable=True

Keyword arguments are especially useful for a function with several parameters, since reusable=True is clearer at the call site than a bare True in the third position.

Multiple Return Values

A return statement can return more than one value, separated by commas. Python packs them into a tuple, which the caller can unpack into separate variables. See Tuples in Depth on the Lists & Tuples page for a worked example.

Recursion

A function can call itself, which is called recursion. Each call should solve a smaller version of the problem, until it reaches a base case that can be answered directly without another call:

import math


def factorial(n):
    if n <= 1:
        return 1              # base case: stop recursing
    return n * factorial(n - 1)   # recursive case: smaller subproblem


print(factorial(5))        # 5 * 4 * 3 * 2 * 1
print(math.factorial(5))   # the standard library already has this

120
120

factorial(5) calls factorial(4), which calls factorial(3), and so on down to factorial(1), which returns 1 without recursing further. Recursion fits naturally when a problem is defined in terms of smaller versions of itself, but for most engineering code, a loop is clearer and avoids Python’s recursion depth limit.

Functions as Values

Functions in Python are values, just like a number or a string. A function can be stored in a variable, passed as an argument to another function, and called later:

def apply_twice(func, value):
    """Call func on value, then call it again on the result."""
    return func(func(value))


def add_ten(x):
    return x + 10


def square(x):
    return x ** 2


print(apply_twice(add_ten, 5))     # add_ten(add_ten(5)) = 25
print(apply_twice(square, 3))      # square(square(3)) = 81

25
81

apply_twice works with whatever function it’s given, calling that function twice without needing to know what it does. A function that accepts or returns another function like this is called a higher-order function.

Variable Scope

Variables created inside a function are local to that function, and only exist while it’s running. Assigning to a variable inside a function does not affect a variable of the same name outside it:

x = 10   # this is a variable in the outer scope

def modify_x():
    x = 99   # this creates a NEW local variable, it does NOT touch the
             # outer x
    print(f"inside function, x = {x}")

modify_x()
print(f"outside function, x = {x}")   # still 10!

inside function, x = 99
outside function, x = 10

x inside modify_x is a different variable from x at the top level, even though they share a name. To actually modify an outer variable from inside a function, you’d need the global keyword, but in this course, prefer passing values in as parameters and getting values out with return instead. It’s much easier to reason about.

Best Practices

  • Use descriptive names for functions and their parameters, so a reader can guess what they do without reading the body
  • Include a docstring describing the purpose of the function and any non-obvious steps
  • Avoid hardcoding values inside a function - pass them in as parameters instead, so the function stays reusable
  • Keep the scope of each function narrow - one task per function

Reading Questions

  1. What are the benefits of user-defined functions in Python?
  2. What keyword ends a function’s execution and sends a value back to the caller?
  3. Describe which variables from a script are, and are not, available inside a function called by that script.
  4. How would you define a function that calculates the volume of a cylinder given its radius and height?
  5. What is the difference between a positional argument and a keyword argument?
  6. If a function is defined as def f(a, b=10):, what value does b take in the call f(5)?
  7. What is a base case, and why does a recursive function need one?
  8. What does it mean for a function to be a “higher-order function”?