Functions

Functions bind inputs to parameters, run code in a local scope, and return results; clear call conventions turn that behavior into a stable interface.

level beginner time 11 min at Standard depth
version Python 3.14
what

A function is a callable object: Python binds arguments to parameters, runs the body in a new local scope, then returns a value or raises an exception.

trap

Defaults are created once when def executes, so a list or dictionary used as a default can accidentally share state across calls.

fix

Express the calling convention with a clear signature, use None for defaults that must be created per call, and make returns and side effects visible.

What it is and why it exists

A function names a behavior and gives it an input, output, and error boundary. A def statement creates a function object and binds the function’s name to that object. Defining it does not run the body; the body runs only when the object is called.

A name in the definition is a parameter , while a value or expression supplied by a call is an argument . In def convert(amount):, amount is a parameter; in convert(25), 25 is an argument. Keeping those terms separate lets you say whether a problem belongs to the interface or to one particular call.

Functions solve more than the problem of copying a few lines. They hide change behind a stable calling convention, so callers depend on names, parameters, return values, and documented exceptions instead of the implementation. Tests can then supply inputs and inspect results without reproducing the whole workflow.

You meet functions in transformations, validation, resource access, callbacks, and class methods. Python also treats functions as ordinary objects: you can assign them to names, put them in containers, pass them as arguments, and return them. This first-class function model underpins decorators, closures, and many standard-library APIs.

How it works

A call expression contains a callable object and some arguments. Python evaluates the callable and each argument expression, then uses the function signature to bind the resulting objects to parameters. Successful binding gives the call its own local namespace and starts the body; failed binding raises TypeError before the body begins.

You can model an ordinary call in this order:

  1. Execute def, create a function object, and bind its name in the defining scope.
  2. Evaluate the callable in the call expression.
  3. Evaluate argument expressions from left to right.
  4. Bind arguments to parameters by position, keyword, and defaults.
  5. Run the body in this call’s local scope.
  6. Hand a value back at return; reaching the end returns None, while an exception propagates up the call stack.

An argument passes the value of an object reference. At the start of the call, a parameter becomes a new local binding to that object. Rebinding the parameter does not change the caller’s name, but mutating the shared object through the parameter is visible to the caller.

Parameter kinds

A function signature says which parameters a function accepts and how callers may supply them. / and * are separators in the signature, not values to pass. They distinguish values that are naturally positional from options that should be named at the call site.

Parameter kindPosition in definitionValid call form
Positional-onlyBefore /By position only
Positional or keywordAfter /, before *By position or name
Variadic positional*itemsCollects extra positional arguments in a tuple
Keyword-onlyAfter * or *itemsMust include the parameter name
Variadic keyword**optionsCollects extra keyword arguments in a dictionary

Without / or *, an ordinary parameter accepts either a positional or keyword argument. Positional-only parameters let an implementation rename a parameter without breaking positional calls. Keyword-only parameters keep Boolean options, units, and timeouts readable at the call site. Read the dedicated topic when you need systematic coverage of *args, **kwargs, and unpacking.

A default expression is evaluated when the function is defined, not for every call. When an argument is absent, binding reuses the saved default object. Immutable objects such as integers, strings, and None are usually safe defaults; mutable objects that should not be shared belong in the body.

Returns, exceptions, and annotations

Every call either returns one value or raises an exception. return expression immediately ends the current call and returns the expression’s value; a bare return and reaching the end of the body both return None. A function that appears to “return multiple values” actually returns one tuple, which the caller may unpack into several names.

An exception is not another return value. An uncaught exception interrupts the function’s normal path and propagates up the call stack. A stable interface makes clear which inputs return normally and which are rejected instead of representing the same failure as None on one path, a string on another, and an exception on a third.

Parameters and returns may carry a type annotation . Annotations help static checkers, editors, and documentation tools understand an interface, but the Python language does not automatically validate arguments or return values against them. Python 3.14 evaluates annotations lazily by default; frameworks that inspect them at runtime should use supported interfaces such as annotationlib.get_annotations().

A docstring is a string literal used as the first statement of the body. It records contract details that names and types cannot express alone, such as units, accepted missing values, external side effects, and exception conditions. A private helper does not always need a long docstring, but callers of a public function should not have to read its implementation to discover those rules.

Examples

These four examples start with an ordinary function, tighten its calling convention, structure its result, and finally pass a function itself as data. Every shown output comes from running the corresponding file.

Define a small boundary

This shipping function has one required parameter, one parameter with a default, and a return annotation. The caller supplies the order amount positionally and names the destination explicitly.

shipping_fee.py
def calculate_shipping(
    subtotal: float,
    destination: str = "domestic",
) -> float:
    """Return the shipping fee for one order."""
    if subtotal < 0:
        raise ValueError("subtotal must not be negative")

    if destination == "international":
        return round(max(12.0, subtotal * 0.08), 2)
    return 5.0


order_total = 80.0
international_fee = calculate_shipping(
    order_total,
    destination="international",
)

print(f"subtotal={order_total:.2f}")
print(f"international fee={international_fee:.2f}")
print(f"domestic fee={calculate_shipping(order_total):.2f}")
subtotal=80.00
international fee=12.00
domestic fee=5.00

The current object reference in order_total binds to subtotal, and the keyword argument binds to destination. The last call omits a destination, so it uses the saved "domestic" default. The function only returns a fee; it does not alter the order amount or take responsibility for business logging.

A negative input takes the exception path. The float annotation does not perform that check; the condition and ValueError in the body provide the real runtime constraint. That keeps the failure policy consistent for direct calls and tests.

Make call sites unambiguous

An order ID works well as positional-only because it is short and stable, while forcing the Boolean priority option to be a keyword prevents an opaque True at the call site. The customer parameter in between accepts either form.

order_label.py
def format_order(
    order_id: int,
    /,
    customer: str,
    *,
    priority: bool = False,
) -> str:
    prefix = "PRIORITY" if priority else "STANDARD"
    return f"{prefix} #{order_id} for {customer}"


print(format_order(1042, "Mina", priority=True))
print(format_order(1043, customer="Noah"))
PRIORITY #1042 for Mina
STANDARD #1043 for Noah

format_order(order_id=1042, customer="Mina") fails because order_id, before /, does not accept keyword form. format_order(1042, "Mina", True) also fails because priority, after *, accepts only a keyword argument. Binding enforces both restrictions before the body starts.

Separators express API design; using more of them is not inherently better. Ordinary parameters are usually enough for a small private helper. Restrictions earn their place in a public interface when a positional value is stable or an option would otherwise be ambiguous.

Use one result shape

This function ignores negative readings, returns None when no valid data remains, and otherwise returns a pair containing the count and mean. The caller handles the missing case before unpacking the normal result.

reading_summary.py
def summarize_readings(
    readings: list[float],
) -> tuple[int, float] | None:
    valid = [reading for reading in readings if reading >= 0]
    if not valid:
        return None

    average = round(sum(valid) / len(valid), 1)
    return len(valid), average


datasets = [[18.0, 21.5, -1.0], [-1.0, -2.0]]

for readings in datasets:
    result = summarize_readings(readings)
    if result is None:
        print("no valid readings")
        continue

    count, average = result
    print(f"count={count}, average={average}")
count=2, average=19.8
no valid readings

The early return None makes the division-by-zero path unreachable. The normal branch always returns the same two-item tuple, so callers need not guess whether the second item exists. A production contract should also say whether a negative reading is ignored or rejected; ignoring it here is part of the example’s contract.

The list comprehension creates a new list, so the function does not modify readings. If it deleted invalid items in place, the caller’s list would also change, and that side effect would need to be explicit in the name and documentation.

Pass behavior as an argument

Function objects can sit in a dictionary and be passed to another function like any other value. apply_pricing only invokes the supplied rule and applies consistent rounding; it need not know whether the rule is a plain function, closure, or another callable object.

pricing_rules.py
from collections.abc import Callable


def apply_pricing(
    subtotal: float,
    rule: Callable[[float], float],
) -> float:
    return round(rule(subtotal), 2)


def regular_price(subtotal: float) -> float:
    return subtotal


def loyalty_price(subtotal: float) -> float:
    return subtotal * 0.9


rules = {
    "regular": regular_price,
    "loyalty": loyalty_price,
}

for customer_type in ("regular", "loyalty"):
    total = apply_pricing(75.0, rules[customer_type])
    print(f"{customer_type}: {total:.2f}")
regular: 75.00
loyalty: 67.50

The dictionary holds function objects, not call results; there are no parentheses after the stored names. rule(subtotal) inside apply_pricing performs the selected call. Passing a strategy works well when behaviors are small and share one signature, while state with several operations is usually clearer as a class.

Callable[[float], float] describes a static interface but does not stop an incompatible object at runtime. You still need static checking and tests for each rule. Continue to closures when behavior must retain configuration or state, and to decorators when behavior must be wrapped before or after a call.

Pitfalls

Fix: use None as a sentinel and create the object in the body with if items is None: items = []. If a shared cache is genuinely part of the contract, put it in an object with a clear name and owner instead of hiding it in a default. This problem is a mutable default argument .

Fix: choose one stable shape for successful results and one explicit policy for “no result.” Expected absence can consistently return None or a domain object. Invalid input or a failed operation usually deserves a specific exception; do not mix several representations for the same meaning.

Fix: decide whether a function transforms its input or updates it in place, then align its name, return, and documentation with that choice. A transformation creates and returns a new object. An in-place operation makes the side effect explicit and tests both object identity and content before and after the call.

Fix: parse and validate untrusted data explicitly at the system boundary, then pass validated objects to internal functions. Use a static checker for development-time mismatches and runtime tests for invalid inputs. They solve different problems.

Fix: ordinary application functions should declare the parameters they support. Reserve variadic forwarding for genuinely generic adapters and decorators, and test that positional arguments, keyword arguments, returns, and exceptions pass through unchanged.

Deep Definition time and call time

Definition time and call time

def is an executable statement. When execution reaches it, Python creates a function object, saves its code, positional defaults, keyword-only defaults, annotation metadata, and necessary references to the defining environment, then binds the name. A def inside a conditional or loop creates its function object only when control reaches it.

The body and default expressions live on different timelines. The body runs for every call, while default expressions run once for that execution of def. A module-level function is normally defined while its module is imported, so its default objects are normally created during import too.

Timing of defaults and annotations

“Once” for a default means once per function definition, not necessarily once for the whole process. If an outer function executes an inner def on every call, each outer call creates a new function object and a new set of defaults. A closure factory can use that fact to give returned functions independent configuration.

Python 3.14 evaluates annotations lazily by default, so their timing differs from ordinary defaults. Annotations still do not change normal call semantics; they matter only to static tools or runtime code that deliberately reads them. Framework authors should not assume that direct access to __annotations__ always produces already evaluated type objects.

Decorator expressions also belong to definition time: Python evaluates them while defining the function and passes the newly created function object through them. The decorated name may end up bound to a different callable, so signature and metadata debugging must distinguish the wrapper from the original function. The complete wrapping rules belong in the decorators topic.

Argument-binding failures

For a user-defined function, binding happens before body logic. A missing required argument, two values for one parameter, an unknown keyword with no **kwargs, or a violation of positional-only or keyword-only rules produces TypeError. Logging, counters, and try blocks in the body have not run, so the function cannot catch these pre-entry binding errors internally.

Argument expressions themselves are evaluated first. If load_order() raises in process(load_order()), process is never called. Side effects inside arguments make evaluation order observable, so call sites are easier to reason about when complex preparation is moved into named statements.

*iterable and **mapping expand arguments before binding. The expanded values still obey the same signature rules; for example, two mappings that provide the same parameter fail. Read the *args and **kwargs topic for the full combination order, forwarding behavior, and repeated unpacking.

Function objects and signatures

A function name is only one binding to a function object. After alias = calculate_shipping, alias and calculate_shipping point to the same object, and calling either runs the same implementation. Parentheses make the call: callbacks.append(calculate_shipping) stores the object, while callbacks.append(calculate_shipping(...)) stores a call result.

User-defined functions expose metadata such as __name__, __qualname__, __doc__, __defaults__, and __kwdefaults__. These attributes help debugging and tooling, but application code should rarely modify the defaults tuple or depend on an internal code object. Metadata describes part of an interface; it does not replace contract tests.

inspect.signature() is the high-level interface for examining the signatures of many callable objects. Its Signature and Parameter objects distinguish parameter kinds, defaults, and annotations, and can attempt to bind arguments without executing the body. Dependency-injection systems, command adapters, and test tools use these capabilities.

Not every callable is a user-defined function. Built-in functions, bound methods, classes, and instances implementing __call__() can all appear in a call expression, and their result rules differ. When an API accepts a general callable, specify its required signature and behavior instead of merely testing whether type(value) is a function.

Docstrings, annotations, and metadata

A docstring is exposed through __doc__, and help() and documentation tools read it. A useful docstring prioritizes the contract callers must follow instead of repeating the name or translating the implementation line by line. Input units, result meaning, observable side effects, and domain exceptions matter more than algorithm narration.

Annotations are metadata and are not limited to type hints, although modern Python code commonly uses them for static types. Python 3.14 provides annotationlib.get_annotations() to retrieve annotations in a selected format, while typing.get_type_hints() also applies type-hint semantics. Reading annotations can trigger lazy evaluation, so do it only for trusted code and handle names that cannot be resolved.

Wrapping can make a function’s visible signature and documentation diverge from the function it proxies. functools.wraps() copies common metadata and sets __wrapped__, which lets many inspection tools find the original function, but it does not prove that arguments, returns, and exceptions are forwarded correctly. Those behaviors still need tests.

Interfaces that can evolve

A signature is public surface that callers depend on. Adding an optional keyword-only parameter is usually easier to make backward-compatible than inserting another positional parameter. Removing a parameter, changing a default behavior, or making a keyword-capable parameter positional-only may break existing code and deserves API-level review in a published library.

A positional-only parameter fits a value whose name should not become contractual. A caller cares only that an operation applies to an object, for example, while the implementation remains free to improve the internal parameter name. A keyword-only parameter fits options with the same type but different meanings because their names remain visible at each call.

Boolean parameters deserve particular care. render(report, True, False) does not reveal what the values control, while render(report, include_header=True, compact=False) does. If combinations begin to represent several modes, an enum or configuration object may be more stable than a growing list of Boolean parameters.

Stability does not require keeping a poor design forever. Find real call sites, use static search and tests to confirm their call forms, then migrate through a deprecation period or a new function name. AI-generated interface refactors especially need keyword-call coverage because tests that exercise only positional calls miss this compatibility surface.

Testing a function contract

Function tests should start from behavior a caller can observe instead of restating every implementation branch. Input objects, returns, raised exceptions, and declared side effects form the test boundary. Local names and internal helper steps are usually not contractual, so a refactor should not force those tests to change.

Normal, boundary, and failure paths

One representative normal case proves only the easiest path. Select inputs exactly at a constraint boundary too: an empty collection, zero, a maximum length, or a value equal to a threshold. One case on either side of a boundary usually says more about a condition than many arbitrary values in the middle.

A failure test should assert a specific exception type and lock down its message only when that message is public contract. Accepting any exception can mistake an accidental KeyError or TypeError for intended validation. Conversely, matching every word of a private diagnostic makes harmless wording changes break the suite.

Test the calling convention itself as well. If a public function promises keyword calls, preserve at least one keyword-call test. A positional-only or keyword-only restriction deserves a failing example that proves the boundary exists. These tests stop a refactor from silently breaking calls while business results still look correct.

A compact test set commonly covers these questions:

  • Does a typical valid input return the correct type and content?
  • Do an empty value, zero, and threshold boundaries follow the contract?
  • Does invalid input raise the expected specific exception?
  • Do repeated calls accidentally share default state or mutate a caller-owned object?

State and side effects

A pure calculation depends only on explicit arguments and returns a result, which makes its tests direct. A function that depends on a clock, randomness, files, a network, or a database can still be tested, but those dependencies should enter through clear boundaries. Secretly reading module globals removes important input from the test report.

A function that mutates an argument needs assertions about both its return and the input object’s final state. Checking only the return can miss a duplicated append, while checking only content can miss that the function returned the wrong object. Identity depends on the contract too: an in-place update normally preserves it, while a transformation normally returns a new object.

Printing is also a side effect. A command-line entry point may print, but an internal calculation usually returns structured data and lets the entry point decide how to display it. The same function can then serve tests, logging systems, web handlers, and other callers without requiring stdout capture to recover its result.

From examples to properties

Example tests check a few known inputs; property tests check a rule that should hold across many inputs. A discount function might never return more than the original amount or less than zero. A sort-key function might promise not to mutate a record. State the property first, then decide whether generated inputs add useful evidence.

Properties do not replace domain examples. A function can satisfy a broad numeric range while choosing the wrong branch at a tax threshold. The strongest small suite combines readable examples, explicit boundaries, precise failure assertions, and only the general properties that carry real information.

Tests that use the implementation’s own formula to calculate the expected result can reproduce the same error. Derive expectations independently from the requirement, then include an input that distinguishes competing implementations. A test adds evidence only when its oracle is independent.

Replacing external dependencies

When a function accepts an external operation as a parameter, a test can supply a small replacement function. The replacement should use the same calling convention and return the shape required by the contract. This controls time, failure, and results without reaching a real network or database.

A recording replacement can also prove that the function under test supplied the right arguments. Record only contract-relevant facts such as a resource ID and retry count instead of freezing every local step. Otherwise, a harmless change in implementation order breaks a test while behavior remains correct.

An overly broad replacement signature may let a test accept a call that production rejects. Test tools with specification support can constrain a double from the real callable, but keep at least one integration test to prove that both sides understand the same parameters and return shape.

Synchronous and asynchronous functions are not interchangeable either. A regular function returns a value, while calling async def first returns a coroutine object; the caller must await it to run the body. A replacement with the wrong kind can create an unawaited coroutine or hide incorrect scheduling behavior.

When an AI generates a replacement, ask it to state the original signature, return contract, and exception contract first. Check that the result preserves positional-only and keyword-only rules and represents failure the same way. Being callable is not the same as implementing the same interface.

Further reading

checkpoint

4 questions · 2 predict-the-output · 1 spot-the-bug

Copy as Markdown Interview bank Edit on GitHub Report an error Was this clear?