A function is a named, reusable block of code that accepts zero or more inputs (parameters), performs a defined computation, and optionally returns a value — encapsulating a repeatable operation behind a single name so it can be invoked many times without repeating its implementation.
The fundamental unit of reusable code
What you'll learn: What a function is, how parameters and return values work, and why functions are the most important tool for managing complexity in any program.
How to read this tab: The Python-specific syntax (def, *args, closures, decorators) is at Functions & Scope. This page explains the universal concept.
A function is a recipe: give it ingredients (inputs), it follows the steps, and hands back a result — and you can use the same recipe as many times as you need.
A function has one job. The single responsibility principle: each function should do one clearly named thing. If you can't name it without using "and", it probably does two things. calculate_and_print_total() should be two functions: calculate_total() and print_total(). This makes functions testable, reusable, and readable.
Defining and calling a function
A function has a name, a list of parameters, and a body of code. Define it once. Call it many times with different values.
def greet(name): # parameter: name
return f"Hello, {name}!"
message = greet("Priya") # argument: "Priya"
print(message) # Hello, Priya!
print(greet("Rahul")) # Hello, Rahul!Default parameters create the function's contract. When you set a default value, you're saying "if the caller doesn't specify this, use this value." This makes functions flexible without requiring the caller to know every option. The one critical rule: never use a mutable object (list, dict) as a default value — it's shared across all calls.
Parameters, arguments, and defaults
A parameter is the placeholder in the definition. An argument is the actual value passed at the call site. Functions can have default parameter values used when the caller doesn't provide them.
def greet(name="World"): # default parameter
return f"Hello, {name}!"
print(greet()) # Hello, World! (default used)
print(greet("Priya")) # Hello, Priya! (argument overrides)Python always returns something — even if you don't say so. A function without a return statement returns None. A function with a bare return also returns None. Use explicit return value for meaningful results. Never return different types from different branches of the same function — that makes callers' code unpredictable.
Return values
return sends a value back to the caller and immediately stops the function. A function with no return implicitly returns None in Python, void in Java/C, and () in Rust.
A function is like a vending machine. You press a button (call with arguments). The machine works internally. It dispenses a result (return value). You don't need to know how the machine works — just what goes in and what comes out.
Functions being values is what makes Python flexible. You can store a function in a variable: greet = print. Pass a function to another function: sorted(items, key=len). Store functions in a list: operations = [add, subtract, multiply]. This enables callbacks, decorators, and higher-order functions — patterns used constantly in Python libraries.
Functions as values
In Python, JavaScript, and most modern languages, functions are first-class values — stored in variables, passed as arguments, returned from other functions.
def double(x): return x * 2
fn = double # store in variable
print(fn(5)) # 10
# Pass as argument to map()
doubled = list(map(double, [1, 2, 3, 4, 5]))
print(doubled) # [2, 4, 6, 8, 10]
square = lambda x: x ** 2 # anonymous function
print(square(4)) # 16Functions let you think at the right level of abstraction. When you write total = calculate_total(cart), you don't need to think about how the total is calculated. The function name is the abstraction. Good function names make programs readable like prose — you understand what the program does before understanding how.
Why functions reduce complexity
Without functions, changing the tax calculation in one place requires finding and updating every copy. With a function, change it once and every call site benefits immediately. Functions are the primary tool for the DRY principle — Don't Repeat Yourself.
✅ Beginner tab complete — check your understanding
- I can explain what a function is in one sentence without using code
- I know the difference between a parameter (the name in the definition) and an argument (the value passed in)
- I know why functions reduce complexity: they let you think about what a block of code does without thinking about how
Functions as values, call stack mechanics, and pass-by-object
What you'll learn: Functions as first-class values (passing functions to other functions). The call stack and what a stack frame contains. Python's pass-by-object-reference and how it differs from pass-by-value and pass-by-reference.
How to read this tab: The pass-by-object-reference section explains the most commonly misunderstood aspect of Python function calls. Read it alongside the mutable default argument gotcha in the Python reference page.
Each function call adds a frame to the call stack. The frame holds: the function's local variables, the arguments passed, and where to return to when the function completes. When the function returns, its frame is removed. Python's default recursion limit (1000) exists because very deep call stacks use large amounts of memory and Python doesn't optimise tail calls.
Call stack mechanics
Every function call pushes a stack frame onto the call stack containing: return address, local variables, and argument values. When the function returns, its frame pops and execution resumes at the return address. Stack overflow occurs when frames accumulate beyond the stack's size limit — Python's default is 1000 frames.
Python uses pass-by-object-reference. The function receives a reference to the same object — not a copy (pass-by-value) and not the variable itself (pass-by-reference). Consequence: if you mutate the object inside the function (list.append()), the caller sees the change. If you reassign the parameter (x = new_value), the caller does NOT see the change. This distinction explains most Python function surprise bugs.
Pass-by-value vs. pass-by-reference vs. pass-by-object-reference
Pass-by-value (C, basic Java types): function gets a copy — mutations don't affect the caller. Pass-by-reference (C++ with &): function gets the caller's variable directly. Python's model: pass-by-object-reference — the function gets a copy of the reference. Mutating a mutable object through the reference is visible to the caller. Rebinding the local name is not.
def append_item(lst, item):
lst.append(item) # mutation visible — same object
def replace_list(lst):
lst = [999] # rebind local — caller unaffected
my_list = [1, 2, 3]
append_item(my_list, 4)
print(my_list) # [1, 2, 3, 4]
replace_list(my_list)
print(my_list) # [1, 2, 3, 4] unchangedPure functions
A pure function always returns the same output for the same input and has no side effects. Pure functions are predictable, testable in isolation, and parallelisable. An impure function may read or write external state (globals, files, databases). Minimise and isolate impurity.
Variadic functions
Python: *args collects positional arguments into a tuple; **kwargs collects keyword arguments into a dict. C: variadic via ... and va_list. JavaScript: ...args rest parameters. Go: vals ...int.
def greet(name)). Argument: the actual value at the call site (greet("Priya")). Many use these interchangeably — the distinction matters for pass-by-value vs. pass-by-reference discussions.lambda x: x*2 creates the same kind of object as def double(x): return x*2. The only restriction: a lambda body must be a single expression.return statement return None implicitly. Calling code that does result = fn() will get None, not an error — which can cause silent bugs if the caller expects a value.✅ Intermediate tab complete — check your understanding
- I can pass a function as an argument to another function
- I know that Python uses pass-by-object-reference: the function gets a reference to the same object, not a copy
- I know why mutating a list inside a function affects the original, but reassigning a parameter doesn't
Pure functions, variadic functions, tail call optimisation
What you'll learn: Pure functions and referential transparency. Variadic functions in C (va_args). Tail call optimisation — why Python doesn't do it and why Scheme requires it.
How to read this tab: Read when studying functional programming or compiler theory.
Activation records and the ABI
A stack frame (activation record) contains: saved program counter (return address), callee-saved registers, local variables, and arguments not passed in registers. The System V AMD64 ABI specifies: first six integer/pointer arguments in RDI, RSI, RDX, RCX, R8, R9; return value in RAX; callee-saved: RBX, RBP, R12–R15. This ABI governs interoperability between C, Rust, Python extension modules, and Go on Linux/macOS.
Tail call optimisation
A tail call is a function call that is the last operation in a function — the result is returned directly. TCO replaces the caller's stack frame with the callee's, turning deep recursion into O(1) stack usage. Guaranteed by Scheme (R7RS), specified but not universally implemented in ES6, available in Haskell/GHC and Scala. Python deliberately does not implement TCO (Guido van Rossum, python-dev 2009) — tail-recursive Python still risks stack overflow.
Python: Language Reference §4.8 — docs.python.org/3/reference/compound_stmts.html. C: ISO C17 §6.5.2.2 — Function calls. ABI: System V AMD64 ABI — refspecs.linuxfoundation.org. Compilers §7.2 (stack frames): Aho et al. 2006, Pearson.
✅ Expert tab complete
- I can identify whether a function is pure (no side effects, same input always gives same output)
- I know Python does NOT perform tail call optimisation — deep recursion will hit RecursionError
- I understand what an activation record (stack frame) contains