Skip to content

Python Functions — Language Specification

1. Spec Reference

  • Primary source: Python Language Reference, §8.7 — Function definitions https://docs.python.org/3/reference/compound_stmts.html#function-definitions
  • §6.2.4 — Calls: https://docs.python.org/3/reference/expressions.html#calls
  • §7.6 — return statement: https://docs.python.org/3/reference/simple_stmts.html#the-return-statement
  • §6.2.9 — yield expressions: https://docs.python.org/3/reference/expressions.html#yield-expressions
  • §4.2 — Naming and binding: https://docs.python.org/3/reference/executionmodel.html#naming-and-binding
  • Python 3.12 standard: All rules here apply to CPython 3.12 unless otherwise noted.

2. Formal Grammar (EBNF)

2.1 Function Definition

funcdef          ::= [decorators] "def" funcname [type_params] "(" [parameter_list] ")"
                     ["->" expression] ":" suite
decorators       ::= decorator+
decorator        ::= "@" assignment_expression NEWLINE
funcname         ::= identifier
type_params      ::= "[" type_param ("," type_param)* "]"   # Python 3.12

2.2 Parameter List

parameter_list   ::= defparameter ("," defparameter)* ["," [parameter_list_starargs]]
                   | parameter_list_starargs
parameter_list_starargs ::= "*" [parameter] ("," defparameter)* ["," ["**" parameter [","]]]
                           | "**" parameter [","]
defparameter     ::= parameter ["=" expression]
parameter        ::= identifier [":" expression]  # name with optional annotation

2.3 Call Expressions

call             ::= primary "(" [argument_list [","]] ")"
argument_list    ::= positional_args ["," starred_and_keywords] ["," kwargs]
                   | starred_and_keywords ["," kwargs]
                   | kwargs
positional_args  ::= assignment_expression ("," assignment_expression)*
                   | "*" expression ("," "*" expression)* ["," starred_keywords]
starred_keywords ::= ("*" expression | keyword_item) ("," "*" expression | "," keyword_item)*
kwargs           ::= "**" expression ("," "**" expression)* [","]
keyword_item     ::= identifier "=" expression

2.4 Lambda Expression

lambda_expr ::= "lambda" [parameter_list_no_default] ":" expression

3. Core Rules and Constraints

3.1 Parameter Types

Python 3 supports five kinds of parameters (PEP 570): 1. Positional-or-keyword: Standard def f(a, b). 2. Positional-only (before /): def f(a, b, /, c) — a and b cannot be passed by name. 3. Keyword-only (after * or *args): def f(*, k) or def f(*args, k). 4. *args — collects extra positional arguments as a tuple. 5. **kwargs — collects extra keyword arguments as a dict.

Parameter order rule:

positional_only /  positional_or_keyword * *args keyword_only **kwargs

3.2 Default Values

  • Default values are evaluated once at function definition time, not each call.
  • Mutable defaults (list, dict, set) are shared across calls — common pitfall.
  • Use None as default and initialize inside the function body.

3.3 Scoping: LEGB Rule Applied to Functions

  • Each function call creates a new local scope (frame).
  • Local variables include: parameters, names assigned in the function body.
  • global name declares a name as referring to the module-level binding.
  • nonlocal name declares a name as referring to the nearest enclosing function scope (not module).
  • A name used in a function but not assigned there is looked up in enclosing → global → built-in scopes.
  • A name assigned in a function is local by default — even reading it before assignment in the same scope raises UnboundLocalError.

3.4 return Statement

  • return [expression_list] exits the function and returns the value.
  • return without a value returns None.
  • A function with no return statement implicitly returns None.
  • In a generator function, return value raises StopIteration(value).

3.5 yield Makes a Generator

  • A function containing yield or yield from is a generator function.
  • Calling it returns a generator object without executing the body.
  • next() on the generator runs until the next yield expression.
  • yield from iterable delegates to a sub-iterator (PEP 380).

3.6 Decorators

  • @expr before def applies the decorator: f = expr(f) semantically.
  • Decorators are evaluated at function definition time.
  • Multiple decorators are applied bottom-up (innermost first).
  • functools.wraps is used to preserve __name__, __doc__, __annotations__, __dict__, __module__, __qualname__, __wrapped__.

3.7 Annotations

  • Parameter and return annotations are stored in function.__annotations__.
  • Annotations have no runtime effect.
  • from __future__ import annotations (PEP 563) makes them lazy strings.
  • inspect.get_annotations() (Python 3.10+) resolves annotations safely.

4. Type Rules (Dunder Methods and Protocols)

4.1 Function Object Attributes

function.__name__         # str: unqualified name
function.__qualname__     # str: qualified name (e.g. "Class.method")
function.__doc__          # str | None: docstring
function.__module__       # str: defining module name
function.__defaults__     # tuple | None: positional default values
function.__kwdefaults__   # dict | None: keyword-only default values
function.__code__         # code object
function.__globals__      # module's global namespace dict
function.__annotations__  # dict of annotations
function.__closure__      # tuple of cells | None (for closures)
function.__dict__         # namespace for arbitrary attributes
function.__wrapped__      # original function if decorated with functools.wraps

4.2 Callable Protocol

object.__call__(self, *args, **kwargs)
# An object is callable if it defines __call__.
# callable(obj) checks for __call__ without invoking it.

4.3 Descriptor Protocol (Relevant for Methods)

# When a function is accessed as a class attribute, __get__ converts it to a method:
function.__get__(obj, objtype=None) -> bound_method | function

4.4 Generator Object Protocol

generator.__next__()              # advance; raises StopIteration when done
generator.send(value)             # send value to yield; returns next yielded value
generator.throw(type[, value[, tb]])  # raise exception at yield point
generator.close()                 # raises GeneratorExit at yield point
generator.__iter__() -> self      # generators are iterators

5. Behavioral Specification

5.1 Argument Binding Rules

When calling f(a, b, c=1, *args, **kwargs): 1. Positional arguments are bound to positional parameters in order. 2. Keyword arguments are matched by name. 3. Excess positionals go into *args. 4. Excess keywords go into **kwargs. 5. If a required parameter (no default) is missing → TypeError. 6. If a parameter receives both positional and keyword argument → TypeError.

5.2 Positional-Only Parameters (/)

  • Parameters before / can only be passed positionally.
  • Prevents callers from breaking on parameter name changes.
  • Example: dict(a=1) — a is a keyword; len([1,2,3]) — obj in C is positional-only.

5.3 Closure Semantics

  • A closure captures variables by reference (via cell objects), not by value.
  • The cell holds the current binding of the variable at the time of access, not definition.
  • Common pitfall in loops: all closures share the same cell.

5.4 functools.wraps Semantics

  • Copies __name__, __qualname__, __doc__, __dict__, __annotations__, __module__ from wrapped to wrapper.
  • Sets __wrapped__ = wrapped.
  • Required for introspection tools (inspect.signature, help()) to see the original signature.

6. Defined vs Undefined Behavior

6.1 Defined

  • Default values evaluated once at function definition time.
  • Positional arguments bind left-to-right.
  • *args is always a tuple; **kwargs is always a dict.
  • return without expression returns None.
  • Generator function body does not execute until next() is called.
  • yield from propagates send() and throw() calls to the sub-iterator.

6.2 Undefined / Implementation-Defined

  • Call stack depth limit: sys.getrecursionlimit() (default 1000 in CPython). PyPy may have a different default.
  • co_* code object attributes: internal representation of bytecode; not part of the language spec.
  • Closure cell internals: cell_contents attribute is CPython-specific.

7. Edge Cases from the Spec (CPython-Specific Notes)

7.1 Mutable Default Argument Trap

def append_item(item, lst=[]):   # lst shared across all calls
    lst.append(item)
    return lst

print(append_item(1))   # [1]
print(append_item(2))   # [1, 2]  — same list!
print(append_item(3))   # [1, 2, 3]

# Fix:
def append_item_safe(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

7.2 Closure Captures Variable Reference (Not Value)

# TRAP: all closures reference the same 'i' cell
funcs = [lambda: i for i in range(5)]
print([f() for f in funcs])   # [4, 4, 4, 4, 4]  — all see i=4

# Fix: default argument captures the value at definition time
funcs = [lambda i=i: i for i in range(5)]
print([f() for f in funcs])   # [0, 1, 2, 3, 4]

7.3 UnboundLocalError — Assignment Makes Name Local

x = 10

def f():
    print(x)   # UnboundLocalError! x is local because assigned below
    x = 20

# Fix:
def g():
    global x
    print(x)   # 10
    x = 20

7.4 nonlocal for Nested Functions

def counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

c = counter()
print(c())   # 1
print(c())   # 2
print(c())   # 3

7.5 yield from Return Value

def sub():
    yield 1
    yield 2
    return "sub_done"   # becomes the value of 'yield from'

def main():
    result = yield from sub()   # result = "sub_done"
    print(f"Sub returned: {result}")
    yield 3

for v in main():
    print(v)   # 1, 2, 3

7.6 Positional-Only Parameters (/)

def add(a, b, /, c=0):
    return a + b + c

add(1, 2)         # OK
add(1, 2, c=3)    # OK
# add(a=1, b=2)   # TypeError: a is positional-only

8. Version History (PEPs and Python Versions)

Feature PEP Version
Generator functions PEP 255 Python 2.2
Decorators PEP 318 Python 2.4
functools.wraps — Python 2.5
yield from PEP 380 Python 3.3
async def / await PEP 492 Python 3.5
Function annotations PEP 3107 Python 3.0
nonlocal statement PEP 3104 Python 3.0
Keyword-only arguments PEP 3102 Python 3.0
Positional-only parameters / PEP 570 Python 3.8
Walrus operator in defaults (N/A; in body) PEP 572 Python 3.8
inspect.get_annotations() PEP 563 Python 3.10
ParamSpec, Concatenate PEP 612 Python 3.10
typing.TypeVarTuple PEP 646 Python 3.11
Type parameters in def PEP 695 Python 3.12

9. Implementation-Specific Behavior

9.1 CPython Frame Objects

  • Each function call creates a frame object containing locals, code object, and execution state.
  • sys._getframe(n) returns the n-th frame up the call stack (CPython-specific).
  • inspect.currentframe() is the portable equivalent.

9.2 CPython Recursion Limit

  • Default: 1000 frames (sys.getrecursionlimit()).
  • Exceeding raises RecursionError (previously RuntimeError).
  • Can be increased via sys.setrecursionlimit() but risks C stack overflow for very large values.

9.3 CPython Function Call Overhead

  • Python function calls are expensive relative to C; each call allocates a frame.
  • Built-in functions (C-level) bypass frame creation.
  • LOAD_FAST bytecode accesses locals by index (fast); LOAD_GLOBAL uses dict lookup.

9.4 PyPy JIT

  • Hot function calls are JIT-compiled; warm-up cost but then near-C speed.
  • Trace-based JIT benefits most from simple loops and numeric functions.

10. Spec Compliance Checklist

  • Mutable default arguments avoided; use None sentinel instead
  • Closures understood: capture by reference, not value
  • global / nonlocal used correctly for non-local assignments
  • UnboundLocalError risk understood for functions that assign a name
  • *args is a tuple, **kwargs is a dict — not lists
  • Decorator application order understood (bottom-up, or right-to-left)
  • functools.wraps used in custom decorators
  • yield from propagates send(), throw(), and return value
  • Positional-only parameters (/) understood
  • Keyword-only parameters (after *) understood
  • Annotations are metadata only; not enforced at runtime
  • Generator function body not executed until next() is called

11. Official Examples (Runnable Python 3.10+)

import functools
from typing import Callable, TypeVar

T = TypeVar("T")

# ----------------------------------------------------------------
# 1. All parameter types in one function
# ----------------------------------------------------------------
def full_params(pos_only, /, normal, *args, kw_only, **kwargs):
    print(f"pos_only={pos_only!r}")
    print(f"normal={normal!r}")
    print(f"args={args!r}")
    print(f"kw_only={kw_only!r}")
    print(f"kwargs={kwargs!r}")

full_params(1, 2, 3, 4, kw_only="k", extra="x")
# pos_only=1, normal=2, args=(3, 4), kw_only='k', kwargs={'extra': 'x'}


# ----------------------------------------------------------------
# 2. Default arguments (evaluated once)
# ----------------------------------------------------------------
import time

def cached_now(ts=None):
    if ts is None:
        ts = time.time()
    return ts

# Calling multiple times returns different values (ts=None each time)
print(cached_now())
print(cached_now())


# ----------------------------------------------------------------
# 3. *args and **kwargs unpacking in calls
# ----------------------------------------------------------------
def add(a, b, c):
    return a + b + c

args_tuple = (1, 2, 3)
kwargs_dict = {"a": 1, "b": 2, "c": 3}

print(add(*args_tuple))    # 6
print(add(**kwargs_dict))  # 6
print(add(1, *[2, 3]))     # 6


# ----------------------------------------------------------------
# 4. Generator function
# ----------------------------------------------------------------
def countdown(n: int):
    while n > 0:
        yield n
        n -= 1

for val in countdown(5):
    print(val, end=" ")   # 5 4 3 2 1
print()

# Generator as iterator
gen = countdown(3)
print(next(gen))   # 3
print(next(gen))   # 2
print(list(gen))   # [1]  — exhausts the generator


# ----------------------------------------------------------------
# 5. yield from
# ----------------------------------------------------------------
def chain(*iterables):
    for it in iterables:
        yield from it

print(list(chain([1, 2], [3, 4], [5])))  # [1, 2, 3, 4, 5]


# ----------------------------------------------------------------
# 6. Closure and nonlocal
# ----------------------------------------------------------------
def make_accumulator(start=0):
    total = start
    def add(n):
        nonlocal total
        total += n
        return total
    return add

acc = make_accumulator()
print(acc(10))  # 10
print(acc(5))   # 15
print(acc(3))   # 18


# ----------------------------------------------------------------
# 7. Decorator
# ----------------------------------------------------------------
def timing(func):
    import time
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.6f}s")
        return result
    return wrapper

@timing
def slow_sum(n: int) -> int:
    return sum(range(n))

result = slow_sum(100_000)
print(result)   # 4999950000


# ----------------------------------------------------------------
# 8. Class decorator
# ----------------------------------------------------------------
def singleton(cls):
    instances = {}
    @functools.wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class Config:
    def __init__(self, val=42):
        self.val = val

c1 = Config()
c2 = Config()
print(c1 is c2)   # True


# ----------------------------------------------------------------
# 9. Positional-only parameters (PEP 570)
# ----------------------------------------------------------------
def normalize(x, y, z, /, scale=1.0):
    import math
    length = math.sqrt(x**2 + y**2 + z**2)
    return (x/length * scale, y/length * scale, z/length * scale)

print(normalize(1, 0, 0))        # (1.0, 0.0, 0.0)
print(normalize(1, 0, 0, scale=2.0))  # (2.0, 0.0, 0.0)
# normalize(x=1, y=0, z=0)  # TypeError


# ----------------------------------------------------------------
# 10. Lambda
# ----------------------------------------------------------------
square = lambda x: x ** 2
print(square(5))   # 25

pairs = [(1, 3), (2, 1), (3, 2)]
pairs.sort(key=lambda p: p[1])
print(pairs)   # [(2, 1), (3, 2), (1, 3)]


# ----------------------------------------------------------------
# 11. Type parameters in function def (Python 3.12, PEP 695)
# ----------------------------------------------------------------
def first[T](lst: list[T]) -> T:
    return lst[0]

print(first([1, 2, 3]))        # 1
print(first(["a", "b", "c"]))  # a


# ----------------------------------------------------------------
# 12. Introspecting function metadata
# ----------------------------------------------------------------
import inspect

def greet(name: str, greeting: str = "Hello") -> str:
    """Return a greeting string."""
    return f"{greeting}, {name}!"

sig = inspect.signature(greet)
print(sig)                          # (name: str, greeting: str = 'Hello') -> str
print(greet.__annotations__)        # {'name': <class 'str'>, ...}
print(greet.__defaults__)           # ('Hello',)
print(greet.__doc__)                # Return a greeting string.

Section Topic URL
§8.7 Function definitions https://docs.python.org/3/reference/compound_stmts.html#function-definitions
§6.2.4 Calls https://docs.python.org/3/reference/expressions.html#calls
§7.6 return statement https://docs.python.org/3/reference/simple_stmts.html#the-return-statement
§6.2.9 yield expression https://docs.python.org/3/reference/expressions.html#yield-expressions
§4.2.2 Scoping and namespaces https://docs.python.org/3/reference/executionmodel.html#resolution-of-names
§3.2 Function object type https://docs.python.org/3/reference/datamodel.html#user-defined-functions
functools Higher-order functions https://docs.python.org/3/library/functools.html
inspect Introspection https://docs.python.org/3/library/inspect.html
PEP 255 Generator functions https://peps.python.org/pep-0255/
PEP 318 Decorators https://peps.python.org/pep-0318/
PEP 380 yield from https://peps.python.org/pep-0380/
PEP 570 Positional-only params https://peps.python.org/pep-0570/
PEP 695 Type parameter syntax https://peps.python.org/pep-0695/