Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Chapter 5: Functions & Modules - Building Reusable Scientific Code

Scientific Computing with Python

Learning objectives

By the end of this chapter, you will be able to:

Prerequisites


Why this chapter matters

Functions are the fundamental building blocks of organized code. Without functions, you’d be copying and pasting the same code repeatedly, making bugs harder to fix and improvements impossible to maintain. But functions are more than just a way to avoid repetition—they’re how we create abstractions, manage complexity, and build reliable software. Whether you’re calculating statistical measures, simulating star cluster dynamics, or processing multi-wavelength observational data, every computational project starts with well-designed functions.

This chapter teaches you to think about functions as contracts between different parts of your code. When you write a function that calculates energy or performs numerical integration, you’re creating a promise: given valid input, the function will reliably return the correct output. This contract mindset helps you write functions that others (including future you) can trust and use effectively. You’ll learn how to choose between positional and keyword arguments, when to use default values, and how to design interfaces that are both flexible and clear.

We’ll explore Python’s scope rules, which determine where variables can be accessed, and learn how seemingly simple concepts like default arguments can create subtle ownership bugs that contaminate calls meant to be independent. You’ll discover how Python’s flexible parameter system enables powerful interfaces, and how functional programming concepts help make state explicit for modern scientific frameworks. By the end, you’ll be organizing your code into modules that can be shared, tested, and maintained professionally—essential skills for collaborative scientific research.

A function contract diagram shows declared inputs entering validation, invalid inputs raising a clear exception, and valid inputs returning a documented result.

Figure 1:A scientific function makes its promises inspectable: what inputs it accepts, which failures it rejects, and what the returned value means.

What to notice: validation can establish a local software boundary, such as a nonempty sequence or compatible units. It does not certify the scientific model that supplied the inputs.

For a first reading, follow this route:

  1. State a function contract.

  2. Make calls communicate intent.

  3. Trace scope and state ownership.

  4. Create an importable scientific module.

  5. Document and test the public contract.

For a return visit, use the closures and functional-interface Deep Dive, the benchmark and cache Deep Dive, or the Reference.

Try this chapter

5.1 Defining Functions: The Basics

A function encapsulates a piece of logic that transforms inputs into outputs. Think of a function as a machine: you feed it raw materials (inputs), it performs some process (the function body), and it produces a product (output). In scientific computing, a function might calculate statistical measures, integrate equations, or transform data — each function performs one clear task that can be tested and trusted.

Your First Function

Let’s start with something every scientist needs — calculating mean and standard deviation:

Mean temperature: 23.92 degC

Let’s break down exactly how this works:

  1. def keyword: Tells Python we’re defining a function.

  2. Function name (calculate_mean): Follows snake_case convention, describes what it does.

  3. Parameters (values): Variables that receive values when function is called.

  4. Docstring: Brief description of what the function does. Include one for public, reused, or non-obvious functions.

  5. Function body: Indented code that does the actual work.

  6. return statement: Sends a value back to whoever called the function.

When Python executes calculate_mean(measurements), it creates a temporary namespace where values = [23.5, 24.1, ...], runs the function body, and returns the result.

Functions Without Return Values

Not all functions return values. Some perform actions like printing results or saving data:

Statistics for Temperature ( degC):
  Mean: 20.920
  Range: [19.800, 22.300]

Returning Multiple Values

Python functions can return multiple values using tuples — perfect for calculations that produce related results:

Mean: 9.950
Std Dev: 0.171
Range: [9.7, 10.2]

Kinetic Energy in CGS Units

Now let’s calculate kinetic energy using CGS units (centimeters, grams, seconds):

Electron kinetic energy: 4.09e-11 ergs
In eV: 25.55 eV

The Design Process: From Problem to Function

Before writing any function, design it first. This prevents the common mistake of coding yourself into a corner:

Energy = 5000.0 ergs
Valid (positive)? True
Valid (in range)? True

5.2 Calls That Communicate Intent

Python provides flexible ways to handle function parameters. Understanding the distinction between positional arguments, keyword arguments, and default values is crucial for designing clear, flexible interfaces. Let’s explore this flexibility through progressive examples.

Positional vs Keyword Arguments

Correct escape velocity: 1.12e+06 cm/s = 11.2 km/s
Wrong (args reversed): 1.19e-13 cm/s
Python accepts the reversed call because both inputs are numbers;
the result is physically meaningless because their units were swapped.

With keywords: 1.12e+06 cm/s

Default Arguments Make Functions Flexible

Default arguments let users omit parameters when standard values suffice:

At t=0: 1000000 atoms
After 5730 years: 500000 atoms
U-238 after 1 billion years: 857244 atoms

The Mutable Default Trap

Here’s a common bug that can quietly contaminate a scientific analysis:

Day 1: [23.5]
Day 2: [23.5, 24.1]
Same list? True

Here’s the correct pattern:

Day 1: [23.5]
Day 2: [24.1]
Combined: [23.5, 24.1]

Variable Arguments (*args and **kwargs)

{'n': 5, 'mean': 3.0, 'std': 1.4142135623730951, 'min': 1, 'max': 5}
Mean only: 2.47
=== Experiment: Test A ===
Configuration:
  temperature_k: 293.15
  pressure_dynes_cm2: 1013250.0
  trials: 10
=== Experiment: Test B ===
Configuration:
  temperature_k: 350
  pressure_dynes_cm2: 2000000.0
  trials: 50
  catalyst: Platinum

Reference: parameter-order rules

Python enforces specific rules about parameter order in function definitions. These rules exist to make functions clearer and prevent ambiguous calls. Let’s build up from simple to complex.

Basic Rule: Required Before Optional

The fundamental rule is that parameters with defaults must come after parameters without defaults:

15
25

The Complete Parameter Order

Python supports different parameter types that must appear in a specific order. Here’s the complete hierarchy:

OrderTypeSyntax ExampleHow to Call
1Positional-onlydef f(x, y, /):Must use position: f(1, 2)
2Standarddef f(a, b):Position or keyword: f(1, 2) or f(a=1, b=2)
3Default valuesdef f(x=10):Optional: f() or f(5)
4*argsdef f(*args):Collects extras: f(1, 2, 3) →\to args=(1,2,3)
5Keyword-only (required)def f(*, x):Must use name: f(x=5) (required)
6Keyword-only with defaultdef f(*, x=10):Optional keyword: f() or f(x=5)
7**kwargsdef f(**kwargs):Collects extra keywords: f(a=1, b=2)

You rarely need all types in one function. Here are practical examples showing common combinations:

3.33
3.3333
Processing 4 points
Normalize: False
Options: {'threshold': 0.5, 'verbose': True}

Understanding Keyword-Only Parameters

Any parameter that comes AFTER *args (or a bare *) can ONLY be passed using its name. This feature makes function calls self-documenting:

Common Patterns You’ll Use

Average of 2, 4, 6, 8: 5.0
Plotting 3 points
  color: red
  linewidth: 2
  marker: o

Understanding the Special Markers

Python uses two special markers to control how arguments can be passed:

5.0
Configuring MyApp
  Debug mode enabled

5.3 Scope and Explicit State

Understanding scope—where variables can be accessed—is crucial for writing bug-free code. Python’s scope rules determine which variables are visible at any point in your program. Without understanding scope, you’ll encounter confusing bugs where variables don’t have the values you expect, or worse, where changing a variable in one place mysteriously affects code elsewhere.

The LEGB Rule

Python resolves variable names using the LEGB rule, searching in this order:

  Inside relativistic: beta = 0.500, gamma = 1.155
Inside calculate: E = 9.45e-07 ergs
Global scope: c = 3.00e+10 cm/s
Final energy: 9.45e-07 ergs = 590120.65 eV

The UnboundLocalError Trap

Count: 2
Global counter: 2

Deep Dive: Closures, Higher-Order Functions, and Functional Interfaces

Closures: Functions That Remember

Rectangle rule: 0.328350
Midpoint rule:  0.333325
Exact:          0.333333
Rectangle error: 0.004983
Midpoint error:  0.000008

Functional Programming Elements

Python supports functional programming—a style that treats computation as the evaluation of mathematical functions. These concepts are essential for modern scientific frameworks like JAX and lead to cleaner, more testable code.

Lambda Functions

Lambda functions are small, anonymous functions defined inline:

Traditional: 25
Lambda: 25

Sorted by x:
  x=0.8, y=1.2
  x=1.5, y=2.3
  x=2.1, y=4.7

Sorted by relative error:
  x=2.1, relative error=1.1%
  x=1.5, relative error=4.3%
  x=0.8, relative error=16.7%

Map, Filter, and Reduce

These functional tools transform how you process data:

Original: [981, 979, 983, 9900, 980, 982, 978]
Filtered: [981, 979, 983, 980, 982, 978]
Removed 1 outliers
Calibrated: ['983.0', '981.0', '985.0', '982.0', '984.0', '980.0']
Final mean: 982.5 cm/s^2

Functions as First-Class Objects

In Python, functions are objects you can pass around:

Grams: [100, 200, 300, 400]
Kilograms: [0.1, 0.2, 0.3, 0.4]

Dynes: [100000.0, 200000.0, 300000.0, 400000.0]
Newtons: [1.0, 2.0, 3.0, 4.0]

Original: [2, 4, 6, 8, 10]
Normalized: ['-1.41', '-0.71', '0.00', '0.71', '1.41']

5.4 Modules as Reusable Scientific Boundaries

As your analysis grows from scripts to projects, organization becomes critical. Modules let you organize related functions together and reuse them across projects.

Creating Your First Module

Save this as statistics_tools.py in your current directory:

"""
statistics_tools.py - Basic statistical calculations.

This module provides fundamental statistical functions.
"""

import math

def mean(data):
    """Arithmetic mean (uses math.fsum for better numerical stability)."""
    if len(data) == 0:
        raise ValueError("mean() requires at least one value")
    return math.fsum(data) / len(data)

def variance(data, ddof=0):
    """
    Variance with optional degrees of freedom adjustment.

    ddof=0 gives population variance (divide by n).
    ddof=1 gives sample variance (divide by n-1).
    """
    n = len(data)
    if n - ddof <= 0:
        raise ValueError(f"variance() requires n > ddof (n={n}, ddof={ddof})")
    m = mean(data)
    return math.fsum((x - m)**2 for x in data) / (n - ddof)

def std_dev(data, ddof=0):
    """Standard deviation corresponding to variance(ddof=...)."""
    return math.sqrt(variance(data, ddof=ddof))

def std_error(data):
    """Standard error of the mean (uses sample std: ddof=1)."""
    return std_dev(data, ddof=1) / math.sqrt(len(data))

if __name__ == "__main__":
    test_data = [1, 2, 3, 4, 5]
    print(f"Test mean: {mean(test_data)}")
    print(f"Test sample std dev: {std_dev(test_data, ddof=1):.3f}")

Using Your Module

# Different import methods

# Method 1: Import entire module
import statistics_tools

data = [10, 12, 11, 13, 10, 11, 12]
m = statistics_tools.mean(data)
s = statistics_tools.std_dev(data)
print(f"Method 1 - Mean: {m:.2f}, Std: {s:.2f}")

# Method 2: Import specific functions
from statistics_tools import mean, std_error

se = std_error(data)
print(f"Method 2 - Standard error: {se:.3f}")

# Method 3: Import with alias
import statistics_tools as stats

var = stats.variance(data)
print(f"Method 3 - Variance: {var:.3f}")

Building a Physics Module

Let’s create a small module-organization sketch for physics calculations. Its named CGS quantities and import boundary are the lesson here; it intentionally does not add complete finite-value and physical-domain checks to every public function. Use kinetic_energy_cgs above as the contract pattern to apply before relying on a calculation in an analysis.

"""
physics_cgs.py - Physics calculations in CGS units.

All calculations use CGS (centimeter-gram-second) units.
"""

# Physical constants in CGS
SPEED_OF_LIGHT = 2.998e10      # cm/s
PLANCK_CONSTANT = 6.626e-27    # erg*s
BOLTZMANN = 1.381e-16          # erg/K
ELECTRON_MASS = 9.109e-28      # g
PROTON_MASS = 1.673e-24        # g
GRAVITATIONAL_CONSTANT = 6.674e-8  # cm^3/(g*s^2)

def kinetic_energy(mass_g, velocity_cms):
    """KE = (1/2) m v^2 in ergs."""
    return 0.5 * mass_g * velocity_cms**2

def momentum(mass_g, velocity_cms):
    """p = mv in g*cm/s."""
    return mass_g * velocity_cms

def de_broglie_wavelength(mass_g, velocity_cms):
    """lambda = h/p in cm."""
    p = momentum(mass_g, velocity_cms)
    return PLANCK_CONSTANT / p if p != 0 else float('inf')

def thermal_velocity(temp_k, mass_g):
    """RMS thermal velocity in cm/s."""
    import math
    return math.sqrt(3 * BOLTZMANN * temp_k / mass_g)

def photon_energy(wavelength_cm):
    """E = hc/lambda in ergs."""
    return PLANCK_CONSTANT * SPEED_OF_LIGHT / wavelength_cm

def gravitational_force(m1_g, m2_g, distance_cm):
    """F = Gm_1m_2/r^2 in dynes."""
    if distance_cm == 0:
        return float('inf')
    return GRAVITATIONAL_CONSTANT * m1_g * m2_g / distance_cm**2

class Particle:
    """Preview of Chapter 6: a simple particle with physics methods."""

    def __init__(self, mass_g, velocity_cms):
        self.mass = mass_g
        self.velocity = velocity_cms

    def kinetic_energy(self):
        return kinetic_energy(self.mass, self.velocity)

    def wavelength(self):
        return de_broglie_wavelength(self.mass, self.velocity)

if __name__ == "__main__":
    # Tiny smoke test
    v_electron = 0.01 * SPEED_OF_LIGHT  # cm/s
    ke = kinetic_energy(ELECTRON_MASS, v_electron)
    wavelength = de_broglie_wavelength(ELECTRON_MASS, v_electron)
    print(f"KE={ke:.2e} ergs, lambda={wavelength:.2e} cm")

After saving that as physics_cgs.py, you can import and use it:

import physics_cgs

v_electron = 0.01 * physics_cgs.SPEED_OF_LIGHT  # cm/s
ke = physics_cgs.kinetic_energy(physics_cgs.ELECTRON_MASS, v_electron)
wavelength = physics_cgs.de_broglie_wavelength(physics_cgs.ELECTRON_MASS, v_electron)

print(f"Electron at 1% c:")
print(f"  Velocity = {v_electron:.2e} cm/s")
print(f"  KE = {ke:.2e} ergs")
print(f"  de Broglie lambda = {wavelength:.2e} cm")

Import Best Practices

# GOOD: Clear, explicit imports
import numpy as np
from pathlib import Path
from math import sin, cos, pi

# BAD: Wildcard imports pollute namespace
# from numpy import *  # Adds 600+ names!
# from math import *   # Conflicts with numpy!

# Example of namespace collision
import math
import cmath  # Complex math module (introduced in Chapter 2)

# Clear which log we're using
real_log = math.log(10)      # Natural log of real number
complex_log = cmath.log(-1)   # Can handle complex numbers

print(f"math.log(10) = {real_log:.3f}")
print(f"cmath.log(-1) = {complex_log}")  # Returns complex number

# If we had used 'from math import *':
# log(10)  # Which log? math or cmath? Unclear!

5.5 Documentation, Tests, and Public Type Hints

Good documentation and testing make your functions trustworthy and reusable. Professional scientific code requires both to ensure reproducibility and reliability.

Documentation Levels

Integral of x^2 from 0 to 2: 2.750
Exact answer: 2.667
Error: 0.083

Testing Your Functions

The assertions below are a compact executable check of named boundary cases. They are not a claim that one script proves a numerical method correct; later chapters develop broader testing and validation practice.

Test 1: Simple roots - PASSED
Test 2: No real roots - PASSED
Test 3: Repeated root - PASSED
All tests passed!

Introduction to Type Hints (Optional Enhancement)

Python supports type hints to document expected types:

Energy: 4.10e-09 ergs
Integer inputs are accepted: 5000.0 ergs
Mean: 3.0, Std: 1.41, Median: 3.0

5.6 Writing Reusable Scientific Python

A notebook is useful for exploration, figures, and interpretation. It is a poor place for the only copy of a calculation that you need to import, test, revise, or reuse in a different analysis. When a calculation becomes part of a result, give it a small public boundary in a module.

This does not require a package or a framework. Start with one module, one test file, and an analysis notebook that imports the module:

spectral-analysis/
├── spectral_tools.py       # public calculations and their documentation
├── tests/
│   └── test_spectral_tools.py
└── analysis.ipynb          # figures, input data, and interpretation

The layout separates three questions:

The separation makes a notebook restart less risky. It does not make a model, a data reduction, or a scientific conclusion correct by itself.

Start with a narrow public interface

The module below defines one unit-aware conversion. Its docstring names the input unit, output unit, valid domain, and failure behavior. The module does not read a file, make a plot, or depend on notebook variables; those are separate responsibilities.

# spectral_tools.py
"""Small, reusable conversions for spectral calculations."""

import math

SPEED_OF_LIGHT_M_S = 299_792_458.0


def wavelength_nm_to_frequency_hz(wavelength_nm: float) -> float:
    """Return frequency in Hz for a positive vacuum wavelength in nm.

    Parameters
    ----------
    wavelength_nm
        Vacuum wavelength in nanometres. It must be finite and positive.

    Returns
    -------
    float
        Frequency in hertz, computed as ``c / wavelength``.

    Raises
    ------
    ValueError
        If ``wavelength_nm`` is not finite or is not positive.
    """
    wavelength_nm = float(wavelength_nm)
    if not math.isfinite(wavelength_nm) or wavelength_nm <= 0.0:
        raise ValueError("wavelength_nm must be finite and positive")

    return SPEED_OF_LIGHT_M_S / (wavelength_nm * 1.0e-9)

Imports should make source visible. In an analysis notebook in the same project directory, use:

from spectral_tools import wavelength_nm_to_frequency_hz

frequency_hz = wavelength_nm_to_frequency_hz(500.0)
print(f"500 nm corresponds to {frequency_hz:.6e} Hz")

Avoid modifying sys.path to make an import work. First check the directory from which Python is running and the name of the module being imported. The Python tutorial on modules explains module names, imports, and the module search path. It also documents why a changed module may need a kernel restart before an interactive session uses the new definition.

Test the contract from a separate file

Place tests in tests/test_spectral_tools.py. A test name should state the behavior it checks. Here, one test uses an exact unit conversion and the other checks the documented invalid-input behavior:

# tests/test_spectral_tools.py
import pytest

from spectral_tools import SPEED_OF_LIGHT_M_S, wavelength_nm_to_frequency_hz


def test_one_metre_wavelength_has_the_speed_of_light_in_hz() -> None:
    assert wavelength_nm_to_frequency_hz(1.0e9) == SPEED_OF_LIGHT_M_S


@pytest.mark.parametrize("wavelength_nm", [0.0, -1.0, float("inf")])
def test_wavelength_must_be_finite_and_positive(wavelength_nm: float) -> None:
    with pytest.raises(ValueError, match="finite and positive"):
        wavelength_nm_to_frequency_hz(wavelength_nm)

From the project root, run:

$ python -m pytest

Running pytest through python -m uses the active interpreter explicitly. That reduces ambiguity when more than one Python environment is installed. The pytest getting-started guide documents discovery, test names, and the command-line workflow.

Keep analysis and reusable code connected without mixing them

Use the module in the notebook; do not copy the function back into a code cell. When a result changes, update the module, the test that identifies the intended behavior, and the notebook’s interpretation together. Then restart the kernel and run the notebook from top to bottom. A green test suite does not show that a notebook used the intended inputs or that a figure has the right labels.

For a small scientific codebase, this checklist is enough:

  1. Put reusable calculations in named .py modules.

  2. Give each public function a unit-aware contract and failure behavior.

  3. Put tests in tests/test_*.py and run python -m pytest from the project root.

  4. Keep notebooks for data provenance, figures, and interpretation.

  5. Record the Python and dependency environment that produced results.

  6. Use version control before a calculation is shared or submitted.

Documentation routes for this section


Deep Dive: Measure and Cache a Stated Workload

Understanding function overhead helps you write efficient code. Let’s measure and optimize!

Function Call Overhead

Function Call Analysis:
Simple function: 7.5 ms
Inline addition: 1.4 ms
Overhead factor: 5.3x

Complex function: 30.6 ms

This is one interpreter and workload; profile the calculation that matters.

Vectorization (A Quick Preview)

You mentioned vectorization in the learning objectives; here’s what it looks like in practice. The core idea: push work into NumPy’s compiled loops instead of Python loops.

import numpy as np
import timeit

rng = np.random.default_rng(0)
n = 200_000
masses = rng.uniform(0.5, 2.0, size=n)         # g (toy)
velocities = rng.normal(0.0, 1.0e7, size=n)    # cm/s (toy)

def ke_loop(m, v):
    out = []
    for mi, vi in zip(m, v):
        out.append(0.5 * mi * vi**2)
    return out

def ke_vec(m, v):
    return 0.5 * m * v**2

repetitions = 3
loop_time = timeit.timeit("ke_loop(masses, velocities)", globals=globals(), number=repetitions)
vector_time = timeit.timeit("ke_vec(masses, velocities)", globals=globals(), number=repetitions)
print(f"Loop ({repetitions} equivalent calls): {loop_time:.3f} s")
print(f"Vector ({repetitions} equivalent calls): {vector_time:.3f} s")
print("This compares one array operation on this machine; it does not predict every NumPy workload.")

Memoization and Cache Keys

Fibonacci(28) = 317811
Without memoization: 35.4 ms
With memoization: 0.044 ms
Local ratio for this first call: 805x

Cache info: CacheInfo(hits=26, misses=29, maxsize=128, currsize=29)

Planck function at 500nm, 5778K: 2.64e+14 erg/(s cm^2 cm sr)
Cache info: CacheInfo(hits=0, misses=1, maxsize=128, currsize=1)

Main Takeaways

Functions transform programming from repetitive scripting into modular, maintainable software engineering. When you encapsulate logic in well-designed functions, you create building blocks that can be tested independently, shared with collaborators, and combined into complex analysis pipelines. The progression from simple functions to modules to packages mirrors how scientific software naturally grows—what starts as a quick calculation evolves into a shared tool used by entire research communities.

The distinction between positional, keyword, and default arguments gives you the flexibility to design interfaces that are both powerful and inspectable. Positional arguments work well for obvious parameters like power(base, exponent), while named arguments make units, switches, and optional behavior visible. The mutable-default trap is a specific ownership bug: a default list is created once and can couple calls that were meant to be independent.

The scope rules and namespace concepts you’ve learned explain why variables sometimes behave unexpectedly in complex programs. Understanding the LEGB rule prevents frustrating bugs where variables have unexpected values or modifications in one place affect seemingly unrelated code. The mutable default argument trap demonstrates why understanding Python’s evaluation model is crucial for writing reliable code. These are not merely academic concepts: they can silently misstate outputs or couple calculations that should be independent.

Functional programming concepts such as pure functions and higher-order functions prepare you for modern scientific frameworks. JAX transformations expect their wrapped functions to make inputs, outputs, and state explicit; pure functions also make a contract easier to isolate in a test. The ability to pass functions as arguments and return them from other functions enables patterns such as the specialized integrators in the Deep Dive.

The performance examples are a method, not a fixed ranking. A function call, vectorized array operation, or cache may help only for a stated workload on a stated machine. Profile the calculation first, then measure an equivalent comparison and record the cache-key or array assumptions that make the result meaningful.

Looking forward, the functions you’ve learned to write here form the foundation for object-oriented programming in Chapter 6, where functions become methods attached to objects. The module organization skills prepare you for building larger scientific packages, while the documentation practices ensure your code can be understood and maintained by others. Most importantly, thinking in terms of functional contracts and clear interfaces will make you a better computational scientist, capable of building the robust, efficient tools that modern research demands.

With functions and modules, you can hide complexity behind clean interfaces. In Chapter 6, you’ll consider small objects when changing state and the operations that preserve its invariants genuinely belong together.

Definitions

argument - The actual value passed to a function when calling it (e.g., in f(5), 5 is an argument)

closure - A function that remembers variables from its enclosing scope even after that scope has finished executing

decorator - A function that modifies another function’s behavior without changing its code

default argument - A parameter value used when no argument is provided during function call

docstring - A string literal that appears as the first statement in a function, module, or class to document its purpose

function - A reusable block of code that performs a specific task, taking inputs and optionally returning outputs

global - A keyword that allows a function to modify a variable in the global scope

keyword argument - An argument passed to a function by explicitly naming the parameter

lambda - An anonymous function defined inline using the lambda keyword

LEGB - The order Python searches for variables: Local, Enclosing, Global, Built-in

memoization - Caching function results to avoid recomputing expensive operations

module - A Python file containing definitions and statements that can be imported and reused

namespace - A container that holds a set of identifiers and their associated objects

package - A directory that makes related modules importable together; a regular package conventionally includes an __init__.py file

parameter - A variable in a function definition that receives a value when the function is called

positional argument - An argument passed to a function based on its position in the parameter list

pure function - A function that always returns the same output for the same input with no side effects

return value - The result that a function sends back to the code that called it

scope - The region of a program where a variable is accessible

sentinel value - A special value (like None) used to signal a particular condition, often as default for mutable arguments

side effect - Any state change that occurs beyond returning a value from a function

*args - Syntax for collecting variable positional arguments into a tuple

**kwargs - Syntax for collecting variable keyword arguments into a dictionary

Key Takeaways

Reference: Function Signatures, Imports, and Typing

Use this section to look up forms after you understand the public function contract. It does not replace stating a calculation’s units, assumptions, and failure behavior.

Quick Reference Tables

Function Definition Patterns

PatternSyntaxUse Case
Basic functiondef func(x, y):Simple operations
Positional onlydef func(a, b, /):Force positional
Default argumentsdef func(x, y=10):Optional parameters
Keyword onlydef func(*, x, y):Force keywords
Variable argsdef func(*args):Unknown number of inputs
Keyword argsdef func(**kwargs):Flexible options
Combineddef func(a, *args, x=1, **kwargs):Maximum flexibility
Lambdalambda x: x**2Simple inline functions

Choosing Argument Types

Argument TypeWhen to UseExample
PositionalObvious meaning, 1-3 paramspower(2, 3)
KeywordMany params, optionalplot(data, color='red')
DefaultCommon valuesround(3.14, ndigits=2)
*argsVariable inputsmaximum(1, 2, 3, 4)
**kwargsConfiguration optionssetup(debug=True, verbose=False)

Module Import Patterns

PatternExampleWhen to Use
Import moduleimport numpyUse many functions
Import with aliasimport numpy as npStandard abbreviations
Import specificfrom math import sin, cosFew specific functions
Import all (avoid!)from math import *Never in production
Relative importfrom . import moduleWithin packages

Common Function Bugs and Fixes

ProblemSymptomFix
Mutable defaultData persists between callsUse None sentinel
UnboundLocalErrorCan’t modify globalUse global or pass value
Missing returnFunction returns NoneAdd return statement
Namespace pollutionName conflictsAvoid wildcard imports
Slow recursionExponential timeAdd memoization
Type confusionUnexpected typesAdd type hints/validation

Recap and next connection

Functions and modules are usually the clearest choice for stateless transformations. Chapter 6 introduces small classes only for entities whose changing state and behavior must remain synchronized.

References

Historical and Technical Sources

  1. Ariane 5 Flight 501 Failure (1996)

    • Lions, J. L. et al. (1996). “ARIANE 5 Flight 501 Failure: Report by the Inquiry Board.” European Space Agency. Paris: ESA. ESA’s Inquiry Board summary.

    • Gleick, J. (1996). “A Bug and A Crash.” The New York Times Magazine, December 1, 1996.

    • Nuseibeh, B. (1997). “Ariane 5: Who Dunnit?” IEEE Software, 14(3), 15-16.

  2. LIGO Gravitational Wave Detection (2015)

  3. Therac-25 Radiation Overdose Disasters (1985-1987)

  4. Mars Climate Orbiter Loss (1999)

Python Documentation

  1. Python Language Reference

  2. Scientific Python Resources

    • Harris, C. R. et al. (2020). “Array programming with NumPy.” Nature, 585(7825), 357-362.

    • VanderPlas, J. (2016). Python Data Science Handbook. O’Reilly Media. ISBN: 978-1491912058.