RUNTIME VALUE CONVERSIONS
Pass supported values and callables between functions, declare the types that your boundaries accept, and use lambdas to capture local variables.
Runtime functions accept positional and keyword arguments, default values, positional-only parameters before /, and keyword-only parameters after *. Parameter and return annotations are optional. A function can return a string, container, instance, or callable without copying a shared mutable object. All supplied arguments are evaluated once, from left to right, after the callee.
The same binding rules apply to top-level functions, lambdas, constructors, and bound methods. Missing required arguments, duplicate values, unknown keyword names, or a positional-only parameter supplied by keyword report an error. Variadic *args, **kwargs, and argument expansion are unsupported. Typed conversion has separate required keyword-call forms.
def label(name: str, /, count: int = 1, *, suffix: str = "!") -> str:
return f"{name.strip()}: {count}{suffix}"
def main() -> int:
print(label(" jobs ", suffix=".", count=3))
print(label("items"))
return 0This prints jobs: 3. and items: 1!. The name is positional-only; the suffix is keyword-only.
Default expressions are evaluated once, from left to right, when each function or lambda is defined. Repeated calls reuse the same stored value. A mutable list or dictionary default is therefore shared; use a None default and create a list inside the function when each call needs its own list.
def remember(value: int, history: list[int] = []) -> int:
history.append(value)
return len(history)
def main() -> int:
print(remember(4), remember(value=7), remember(9, history=[]))
return 0This prints 1 2 1. Module function and method defaults are initialized in source order once per C context, before its first public function call. They may use supported expressions and earlier module definitions; referencing a later definition at that point reports a name error.
def remember(value: int, history: list[int] | None = None) -> int:
if history is None:
history = []
history.append(value)
return len(history)
def main() -> int:
print(remember(4), remember(7))
return 0
This prints 1 1. Each omitted history creates a separate list. Passing an existing list deliberately still shares that list.
| Annotation | Accepted value |
|---|---|
bool, int, float, str, None | The corresponding runtime kind. Integer checks also admit Boolean values; float checks require floating values. |
| A declared class name | An instance of that class. |
int | str, Counter | None | A value accepted by either annotation. |
list, tuple, dict | The named container kind without an element constraint. |
dict[K, V] | A dictionary whose current keys satisfy K and values satisfy V. |
list[T] | A list whose current elements satisfy T. |
tuple[T, U] | A fixed-length tuple with separately checked positions. |
tuple[T, ...] | A tuple of any admitted length whose elements satisfy T. |
object | Any initialized supported value, including a callable. |
Use annotation names directly. Imports from typing, quoted forward annotations, and general annotation expressions are unsupported.
def choose(ready: bool) -> int | str:
result: int | str
if ready:
result = 4
else:
result = "waiting"
return result
choose(True) returns 4; choose(False) returns "waiting".
Annotations check arguments on entry, returned values, stores to annotated local names, and stores to declared fields. An annotation without an initial value leaves the local uninitialized. Every executed read still needs an earlier assignment.
Checks do not coerce values: use 1.0 or float(value) when a float is required. An unannotated binding may change type; an annotated binding must continue to satisfy its declared annotation. Redeclaring a name with an incompatible annotation is rejected.
Container checks inspect elements at the boundary being crossed. They do not permanently bind a type schema to every alias. For example, appending text through an unannotated alias of a list does not itself enforce an earlier list[int] declaration; passing that list to an annotated list[int] parameter then fails. This checking is part of the C conversion contract; Python itself does not enforce ordinary annotations at runtime.
def apply(operation, value: int) -> int:
return operation(value)
def increment(value: int) -> int:
return value + 1
def calculate(value: int) -> int:
return apply(increment, value)
calculate(4) returns 5. Lambdas and bound methods may be passed in the same way.
def make_offset(start: int):
offset = start
transform = lambda value: value + offset
offset += 1
return transform
def calculate(value: int) -> int:
transform = make_offset(3)
return transform(value)
calculate(10) returns 14. The lambda captures the live local variable, so it observes the changed offset. Captured storage survives the creating function and remains valid for the C context's lifetime.
Lambdas use the same positional, keyword, default, and keyword-only parameter forms as functions, with a supported expression body. They can be nested, returned, passed to other functions, or stored in containers. Distinct lambda creations have distinct identity.
def main() -> int:
operations = [lambda value: value + offset for offset in range(3)]
print(operations[0](10), operations[2](10))
return 0
Both calls print 12: the lambdas share the comprehension variable, whose final value is 2. A default parameter captures the current value when each lambda is created:
def main() -> int:
operations = [lambda value, offset=offset: value + offset
for offset in range(3)]
print(operations[0](10), operations[2](value=10))
return 0This prints 10 12. A captured mutable object is still shared; a default saves its reference, not a deep copy.
Functions have at most 64 parameters and calls accept at most 64 explicit arguments. Functions have at most 1,024 local bindings; a lambda can capture up to 256 local cells. Calls are checked against a depth limit of 128. Invalid argument counts, incompatible annotations, uninitialized reads, and resource exhaustion report runtime errors. See C caller error handling.