Functions, Parameters, Return Values & Calling Conventions

In Python, functions are first-class objects created at runtime by the def statement. Understanding CPython’s argument binding protocol, frame value stack execution, positional-only (/) and keyword-only (*) parameters, and the mutable default parameter initialization trap is essential for writing robust, high-performance software.

This chapter details function object architecture (PyFunctionObject), CPython call stack mechanics, positional-only/keyword-only parameter syntax (PEP 570), and default argument instantiation.


1. Function Object Architecture (PyFunctionObject)

When Python executes a def statement, it compiles the function body into a PyCodeObject and wraps it in a PyFunctionObject struct on the heap:

CPython Function Struct (PyFunctionObject):

[ PyFunctionObject @ 0x7F9A ]
β”œβ”€β”€ func_code      --> [ PyCodeObject ] (Bytecode, variable names, constants)
β”œβ”€β”€ func_globals   --> Pointer to module globals dict (for LEGB lookup)
β”œβ”€β”€ func_defaults  --> Tuple of default argument values (evaluated ONCE at def time!)
β”œβ”€β”€ func_kwdefaults--> Dict of keyword-only default values
β”œβ”€β”€ func_closure   --> Tuple of PyCellObject refs (for captured free variables)
└── func_annotations--> Dict of type annotations

Because func_defaults is attached directly to the PyFunctionObject struct when the def statement is evaluated, default argument values persist across all subsequent function calls.


2. Parameter Calling Conventions: Positional-Only (/) & Keyword-Only (*)

Python (PEP 570 & PEP 3102) allows explicit control over parameter binding syntax:

def configure_service(host, port, /, timeout=30, *, use_ssl=True):
    pass
Parameter Syntax Rules:

  def func(pos_only, /, standard_param, *, kw_only):
           β””β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”˜
          Positional     Positional or    Keyword Only
            Only           Keyword         (Must pass as name=val)
  1. Positional-Only Parameters (/): Parameters left of / cannot be passed as keyword arguments. This allows changing parameter names in library code without breaking caller code.
  2. Keyword-Only Parameters (*): Parameters right of * must be passed as explicit keyword arguments (use_ssl=True). This prevents accidental parameter positional displacement bugs.

3. The Mutable Default Argument Trap

The most famous trap in Python stems from func_defaults being evaluated once at module load time:

# THE TRAP: Default list instantiated ONCE during 'def' evaluation!
def append_to_list(element, target_list=[]):
    target_list.append(element)
    return target_list

print(append_to_list(1))  # [1]
print(append_to_list(2))  # [1, 2] (target_list reused the SAME list instance!)

Production Fix:

Always use None as the default marker for mutable parameters and instantiate a new container inside the function body.


4. Production Trade-offs & Frame Overhead

  • *args and **kwargs Overhead: *args packs extra positional arguments into a tuple; **kwargs packs keyword arguments into a dictionary. While flexible, packing and unpacking args adds a minor overhead. Use explicit parameters for hot-path public APIs.
  • Function Call Cost: Function calls in Python involve creating a PyFrameObject on the C stack. For micro-benchmarks involving simple math operations inside hot loops, inlining simple logic avoids frame allocation overhead.
Display Options
Appearance
Text Size
100%