Generics¶
A function can declare type parameters the way Python 3.12 spells them:
Inference and bounds¶
A call infers the type arguments from what it passes and checks each against
its bound (E1721). The call has the declared return type with the type
arguments substituted: largest(1, 2) is an int, largest(1.5, 2.5) a
float.
An unbounded parameter has no operators, because nothing says it does.
Protocol bounds¶
A bound that is a Protocol lends its methods, to operators and to attribute calls alike:
from typing import Protocol
class Named(Protocol):
def name(self) -> str: ...
def label[T: Named](thing: T) -> str:
return "at " + thing.name()
A project class is an instance of a Protocol when its members cover the Protocol's.
Monomorphization¶
Native code monomorphizes. A generic called from a native function is lowered
once per tuple of type arguments, under a name that spells them, and the call
goes straight to that instance. The generic itself keeps its Python body for
every other caller. ppy emit ir shows the instances, marked ppy.generic.
[tool.ppy.generics] bounds the process:
max-specializationsper generic (E1722)max-depthof a type argument
A generic that calls itself with its own parameter wrapped in a type is
refused outright (E1723), because its specializations never end.
Static dispatch¶
a + b on a value class inside native code calls the class's own __add__,
lowered like any native function. Native code does not fall back to Python's
dynamic dispatch. A class without a native operator is refused with the
reason.
Examples: Generics, Value classes.