typing module
typing exists so type-annotated code can import it without
ModuleNotFoundError. No runtime type checking happens. The forms are inert
marker objects: the generic ones can be subscripted, building an alias
(see typing aliases), Union and Optional build unions
(see Unions), and the rest raise CPython’s TypeError.
Annotations are compiled and evaluated lazily, as in CPython 3.14 (see Annotations).
Any, Optional, Union, List, Dict, Tuple, Set, FrozenSet,
Callable, Type, Sequence, Mapping, Iterable, Iterator,
Generator, ClassVar, Final, Literal, TypeVar, Generic,
Protocol, Annotated, Self, Never, NoReturn, TYPE_CHECKING,
ForwardRef (the annotationlib type) and
get_type_hints.
TYPE_CHECKING is False, as in CPython at runtime.
get_args,get_origin,cast,assert_type,assert_never,overload,final,runtime_checkable,NewType,NamedTuple,TypedDict,dataclass_transform,ParamSpec,Concatenate,Unpack,TypeAlias,TypeAliasType,LiteralString,evaluate_forward_ref.- Annotation introspection on modules: a module-level
x: intis checked and dropped, so the module’s__annotations__reads as{}(see language.md). Functions and classes have theirs; see below.
Annotations follow CPython 3.14 (PEP 649): an annotated def or class body compiles its annotations into an
__annotate__(format) function, __annotations__ is evaluated from it on first access, and under
from __future__ import annotations the stored values are strings instead (see
language.md).
A class keeps the function in its namespace as __annotate_func__ and the evaluated dict as __annotations_cache__,
the keys CPython uses; a namespace handed to type() may hold an __annotate__ or an __annotations__ of its own.
Divergences:
f.__annotations__is recomputed on every access. A function has no__dict__to cache it in, sof.__annotations__ is f.__annotations__isFalse, a change to the dict is lost, an annotation with a side effect runs again, andf.__annotations__ = {...}raisesAttributeError. A class evaluates once and caches, as in CPython.hasattr()andgetattr()with a default evaluate__annotations__synchronously, so anAttributeErroran annotation raises can answerFalseor the default as in CPython; an annotation that calls an external function raisesNotImplementedErrorthere, where CPython evaluates it, and a lazy host attribute reads as undefined (see classes.md).obj.__annotations__andgetattr()without a default can suspend for the host.C.__annotate__raisesAttributeError. The function only resolves class-body names when run under the class namespace, whichC.__annotations__andget_annotationsdo, so it is not exposed on the class.f.__annotate__is exposed on functions.- Enclosing-function locals are not visible.
An annotation resolves names through the class namespace (a class body’s own annotations) and the module globals,
so
def outer(): T = int; def f(x: T): ...raisesNameErroronf.__annotations__where CPython capturesT, and a method’s annotations do not see class-body names where CPython’s do. - A class created by
exec()evaluates in the session globals, not theexecglobals dict. A function created byexec()uses its dict, as in CPython. - Only simple
name: Ttargets are recorded, as in CPython. A bareobj.attr: Tcontributes nothing to__annotations__on either, but CPython still evaluates the target expression:undefined.attr: intraisesNameErrorthere and is silently dropped by Monty. With a value (obj.attr: T = v) Monty raisesNotImplementedError. - Binding
__annotations__explicitly in a class body that also has annotated names raisesNotImplementedErrorunderfrom __future__ import annotations, where CPython merges the collected strings into it. Without the import the binding is an ordinary member thatC.__annotations__returns, as in CPython. - A hand-written
__annotate__closure runs with its real cells inFORWARDREFandSTRINGformat, where CPython substitutes a stringifier for an empty cell.
get_type_hints(obj, globalns=None, localns=None, include_extras=False, *, format=None) evaluates the annotations of a function or class.
It follows CPython 3.14’s algorithm: the object’s __annotate__ runs (or its stored __annotations__ dict is read), then a string (forward reference) evaluates in globalns / localns, list['Foo'] / Optional['Foo'] arguments are resolved at any depth, and Annotated metadata is stripped unless include_extras.
Under from __future__ import annotations every annotation is such a string, so it evaluates in globalns / localns only.
format= takes the annotationlib.Format members: VALUE (the default), FORWARDREF, which returns an unresolved name as an annotationlib.ForwardRef, and STRING, which returns the source text.
Divergences:
- Name resolution follows Annotations: enclosing-function locals, and class-body names in a method’s annotations, are not visible.
globalns/localnsmust be a dict orNone. Anything else raisesTypeError: get_type_hints() globalns must be a dict, not int, where CPython fails later with an unrelated message.- A function’s annotations are re-evaluated on every call, including any side effects, where CPython evaluates them once (on
__annotations__) and caches the result; a class’s are evaluated once and cached. A builtin rebound after the first call (exec('int = str')) is therefore seen by the next call for a function. - Modules record no annotations, so
get_type_hints(module)is{}. - Evaluation errors show a
<string>frame in the traceback, where CPython shows itstyping/annotationlibframes. - Host functions and host classes return the hints the host answers with, unevaluated in the sandbox; see host-values.md.
The host evaluates them in
VALUEform whatever theformat:FORWARDREFraises where CPython would return aForwardRef, andSTRINGraisesNotImplementedError. FORWARDREFnever asks the host for a name. A name the host would supply through a name lookup is aForwardRefin this format where CPython resolves it.- A
FORWARDREFreference holds the resolved text. CPython keeps the objects an unresolved expression used aside under placeholder names, sox: int | LatergivesForwardRef('int | Later')with a__forward_arg__of'__annotationlib_name_1__ | Later'; Monty writesint | Laterinto__forward_arg__and resolvesintby name when the reference is evaluated. A class is written by its bare name:Foo | Laterwhere CPython’s__resolved_str__reads__main__.Foo | Later. - Comparison operators are not stringified.
x: Later == 1evaluates toFalseinFORWARDREFwhere CPython returnsForwardRef('Later == 1'). STRINGis the stringized source. A starred argument stays as written (x: Later(*[2])gives'Later(*[2])') where CPython reconstructs the call it made:'Later(2)'.
Subscripting a builtin type builds a types.GenericAlias, but only for
list, tuple, dict, set, frozenset, type, collections.deque,
collections.defaultdict, collections.Counter, functools.partial,
re.Pattern and re.Match. Every other type raises
TypeError: type 'int' is not subscriptable, including ones CPython
parameterizes:
enumerate(a builtin function in Monty, not a type).collections.namedtupleclasses, which in CPython inherittuple.__class_getitem__;Point[int]raises.- User classes:
__class_getitem__is not looked up, soFoo[int]raises whether or not the class defines it.
Divergences in the aliases themselves:
- No
typesmodule.type(list[int])reprs as<class 'types.GenericAlias'>, butimport typesraisesModuleNotFoundError, soisinstance(x, types.GenericAlias)cannot be written; comparetype(x) is type(list[int])instead. - Not iterable. CPython iterates an alias to yield its starred form
(
*tuple[int, ...]); Monty raisesTypeError: 'types.GenericAlias' object is not iterable. - Argument reprs use Monty’s type names. A user class prints its bare name
(
list[Foo]where CPython printslist[__main__.Foo], see classes.md), andcollections.Counter[str]prints asCounter[str]. - An unhashable argument names the alias.
hash(list[[1]])raisesTypeError: unhashable type: 'types.GenericAlias'where CPython names the argument ('list'), as with a tuple holding a list. - A namedtuple subscript is one argument.
list[Point(int, str)]keeps the namedtuple as its single argument, where CPython’sPyTuple_Checkunpacks it intolist[int, str]. - A cycle through the arguments prints as
.... CPython’s alias repr has no recursion guard and raisesRecursionErroronl = []; l.append(list[l]); repr(l); Monty prints[list[[...]]]. - No
__class__.list[int].__class__raisesAttributeError, as it does for every builtin value (see builtins.md); usetype(list[int]). - No
__orig_class__on call results. CPython sets it on the result of calling an alias when the object accepts attributes, sofunctools.partial[int](f).__orig_class__isfunctools.partial[int]; Monty’s partial objects take no attributes, so the lookup raisesAttributeError.partialis the only subscriptable type whose CPython instances accept it.
Subscripting a generic typing form — List, Dict, Tuple, Set, FrozenSet, Callable, Type, Sequence,
Mapping, Iterable, Iterator, Generator, ClassVar, Final, Literal and Annotated — builds an alias
with CPython’s repr (typing.List[int]), __origin__, __args__, __parameters__ and, for Annotated,
__metadata__; equality and hashing follow the form and its arguments, a None argument becomes NoneType as in
CPython, and calling an alias other than Annotated (which calls its type) raises CPython’s TypeError.
Optional[X] builds a union (see Unions); Any, TypeVar, Self, Never, NoReturn, Generic and
Protocol raise CPython’s TypeError when subscripted.
Divergences:
- The alias type is
types.GenericAlias.type(List[int])is the type oflist[int], where CPython hastyping._GenericAlias; the two aliases still compare unequal (List[int] != list[int]). __origin__ofCallable,Sequence,Mapping,Iterable,IteratorandGeneratoris thetypingform, where CPython gives thecollections.abcclass, which Monty does not have.- Arguments are not validated beyond
Annotatedneeding two,ClassVar/Finalone andCallablea parameter list and a result:List[int, str]andType[3]are accepted as written where CPython raises. - Dunder attributes raise
AttributeError:__name__and__qualname__, where CPython gives the form’s name ('List'), and__module__,__doc__,__class__and__dict__, where CPython gives the alias’s own values. Anything else is looked up on the origin, as in CPython.
int | None, typing.Union[int, str] and typing.Optional[int] build a
typing.Union, as in CPython 3.14. Divergences:
- Member reprs use Monty’s type names, as in a generic alias:
Foo | Nonewhere CPython prints__main__.Foo | None. - An unhashable member names the union.
hash(int | list[[1]])raisesTypeError: unhashable type: 'typing.Union'where CPython names the member. - No attributes beyond
__args__,__origin__and__parameters__.__class__,__or__and the other dunders CPython exposes raiseAttributeError. - String members stay strings.
Optional['Foo']is'Foo' | Nonewhere CPython wraps the string asForwardRef('Foo'). - Special forms are accepted as members.
Optional[typing.Final]buildstyping.Final | Nonewhere CPython raisesTypeError: Plain typing.Final is not valid as type argument. - Neither aliases nor unions cross the host boundary. One built in the
sandbox reaches the host as its repr string. Passed in from the host, a
list[int]degrades to an external function (it is callable, so it is treated like any unmodeled class) and anint | Noneis rejected withMontyConversionError; neither has aMontyObjectform. Their type objects round-trip:types.GenericAliasby identity, and Monty’styping.Unionas the host’stypes.UnionType, which istyping.Unionitself only from Python 3.14.