Types and annotations
Values are Python objects with Python types. Annotations are metadata that tools and readers use; nothing checks them at a call.
Writing an annotation
:: attaches an annotation to a definition, a formal, or a return:
var total :: int 0
def double :: int [(n :: int)]:
(* n 2)
class Point []:
var x :: int 0
var y :: int 0
An annotated formal is parenthesized inside the formals list:
[(n :: int)]. The parentheses group the name with its annotation, so
the formals list still reads as one parameter. A bare [n :: int] is a
compile error that names the parenthesized spelling.
The annotation is an ordinary expression evaluated in the defining
scope. A string annotation, :: "Request", stays a lazily resolved
forward reference as it does in Python.
Where it lands
A function’s annotations go to __annotations__ and inspect.signature
reports them:
import inspect
(print double.__annotations__) ;; {'n': <class 'int'>, 'return': <class 'int'>}
(print (inspect.signature double)) ;; (n: int) -> int
A class’s annotated var forms go to the class __annotations__,
which is what dataclasses.dataclass reads to generate __init__,
__repr__, and __eq__. Any Python tool that consumes annotations,
pydantic, FastAPI, typing.get_type_hints, sees the same objects it
would see from a Python definition.
Checking at runtime
An annotation does not check anything. A function that wants a guarantee checks it:
def take [count]:
when (< count 0):
raise (ValueError "count must be non-negative")
...
instance? and subclass? are the type tests: (instance? p Point).
Defining a class binds only its name; no Point? predicate is created.
Contracts by convention
A public function states its expectations in three places: its name, its parameter names and annotations, and an early check with a clear error where a bad value would otherwise fail somewhere confusing later. Annotations do the second job well and the third not at all.