def greeting(name: str) -> str:
return 'Hello ' + nameNothing in that function is enforced. The typing documentation says it plainly in a note at the top: the Python runtime does not enforce function and variable type annotations, and they can be used by third party tools such as type checkers, IDEs, and linters. PEP 484 says the same thing from the other direction. While these annotations are available at runtime through the usual __annotations__ attribute, no type checking happens at runtime. Instead, the proposal assumes the existence of a separate off-line type checker which users can run over their source code voluntarily, and PEP 484’s own summary of what such a checker is: essentially a very powerful linter.
The idea
Python’s type system is a side channel, not a runtime mechanism. The interpreter’s entire contribution is to record annotation expressions and hand them back when asked; every judgment about whether a program is well-typed happens in a different process, run by a tool that is not the interpreter. That is what makes the system gradual rather than optional-in-name-only: because nothing at runtime depends on the annotations, a codebase can be annotated one function at a time, and unannotated code stays legal and unchecked forever by design.
What the syntax was before it meant anything
PEP 3107 introduced syntax for function annotations, but the semantics were deliberately left undefined. Annotations were a place to put an expression next to a parameter, and the language committed to nothing about what it meant. PEP 484’s motivation was that there had been enough third-party usage for static type analysis that the community would benefit from a standard vocabulary and baseline tools in the standard library. Its contribution was a provisional module of standard definitions plus conventions for situations where annotations are not available.
Notably PEP 484 did not close the door on other uses. It explicitly does not prevent other uses of annotations, nor does it require or forbid any particular processing of annotations, even when they conform to the specification. The comparison the PEP draws is to PEP 333 and web frameworks: the value is coordination, not enforcement.
Two design choices from that era still shape the ergonomics. The bracket notation Sequence[int] was chosen so that no new syntax needs to be added to the language, and it works at runtime by implementing __getitem__() in the metaclass, though its significance is primarily to an offline type checker. And the whole proposal is strongly inspired by mypy.
PEP 484’s stated priority ordering is worth carrying: of static analysis, potential runtime type checking, and code generation, static analysis is the most important, including off-line checkers such as mypy plus a standard notation IDEs can use for code completion and refactoring.
The non-goals are the load-bearing part
PEP 484’s Non-goals section is short and unusually blunt. Runtime type checking is not in scope: while the typing module contains some building blocks for it, in particular get_type_hints(), third party packages would have to be developed to implement specific runtime type checking functionality, for example using decorators or metaclasses. Using type hints for performance optimizations is left as an exercise for the reader.
Then the sentence that settles the argument about where Python is headed: Python will remain a dynamically typed language, and the authors have no desire to ever make type hints mandatory, even by convention.
The gradualism follows mechanically from the annotation rules. Any function without annotations should be treated as having the most general type possible, or ignored, by any type checker, and functions with the @no_type_check decorator should be treated as having no annotations. For a checked function, the default annotation for arguments and for the return type is Any. The self-shaped exception is carved out: if the first argument of an instance or class method is not annotated, it is assumed to have the type of the containing class for instance methods, and a type object type corresponding to the containing class object for class methods.
Any is not a type, it is a hole
Any is the piece that makes gradual typing work and the piece most often misread. A static type checker will treat every type as assignable to Any and Any as assignable to every type. That is bidirectional, and it is the whole trick.
The consequence is that no type checking is performed when assigning a value of type Any to a more precise type. The typing documentation’s own example assigns an Any-typed name holding an int into a name declared str, and reports that the static type checker did not report an error even though the variable receives an int value at runtime. Any operation or method call is permitted on a value of type Any. This behavior allows Any to be used as an escape hatch when you need to mix dynamically and statically typed code.
object is the deliberate contrast. Every type is a subtype of object, but unlike Any the reverse is not true, so object is not a subtype of every other type. When the type of a value is object, a type checker will reject almost all operations on it, and assigning it to a variable or using it as a return value of a more specialized type is a type error. Any says “stop checking”; object says “check, and I promise nothing beyond existence.”
And unannotated code is Any-shaped by default: all functions without a return type or parameter types implicitly default to using Any. That is the mechanism by which a half-annotated codebase does not immediately drown in errors, and also the mechanism by which a half-annotated codebase quietly checks much less than its authors assume. The relationship between checked and unchecked regions is the same tension covered in type systems, goals, and guarantees.
NewType is static-only, and its runtime residue is nearly nothing
UserId = NewType('UserId', int)gives a checker a type it treats as if it were a subclass of the original type, soget_user_name(UserId(42351))passes whileget_user_name(-1)fails. You may still perform allintoperations on aUserId, but the result will always be of typeint. At runtime the statementDerived = NewType('Derived', Base)makesDeriveda callable that immediately returns whatever parameter you pass it, soDerived(some_value)does not create a new class or introduce much overhead beyond that of a regular function call. The documentation gives the sharpest possible statement of the identity: the expressionsome_value is Derived(some_value)is always true at runtime.
When the annotation expression actually runs
The one place the runtime genuinely participates is evaluation timing, and that has changed twice. Python’s original semantics for annotations required them to be eagerly evaluated, at the time the annotated object was bound, which caused chronic problems for static type analysis users due to forward-reference and circular-reference problems. PEP 563’s answer was stringized annotations, in which annotations were automatically converted into strings by Python, and that solved the forward-reference and circular-reference problems but in turn caused chronic problems for runtime users of annotations.
PEP 649, “Deferred Evaluation Of Annotations Using Descriptors,” is the third approach and targets Python 3.14. It adds a new dunder attribute __annotate__ to functions, classes, and modules, holding a reference to a function which computes and returns that object’s annotations dict. At compile time the compiler writes the expressions computing the annotations into their own function and stores a reference to it in __annotate__; __annotations__ is redefined as a data descriptor which calls this annotation function once and caches the result. The point is that this delays the evaluation of annotations expressions until the annotations are examined, which solves many circular reference problems. PEP 649 also adds a keyword-only format parameter to inspect.get_annotations and typing.get_type_hints, with inspect.VALUE = 1 as the default returning the conventional Python values.
The practical payoff shows up in typing.TYPE_CHECKING, a constant assumed to be True by static type checkers and False at runtime. A module which is expensive to import, and which only contains types used for typing annotations, can be safely imported inside an if TYPE_CHECKING: block. That prevents the module from being imported at runtime, and because annotations are not eagerly evaluated, using undefined symbols in annotations is harmless as long as you do not later examine them. This is the clearest demonstration of the side-channel thesis: an annotation can name something that does not exist in the running process.
The escape hatch has a cost the checker cannot report
if TYPE_CHECKING:imports and lazily evaluated annotations mean a name in an annotation may be unresolvable at runtime. Code that introspects annotations, a serializer, a dependency injector, a validation library, hitsNameErroron exactly those names. The documented recourse isannotationlib.get_annotations()with aformatofannotationlib.Format.STRINGorannotationlib.Format.FORWARDREF, which retrieves the annotations without raisingNameError. Static-only and runtime-consumed annotations are different use cases sharing one syntax, and the split is where most annotation bugs live.
Related Notes
- Type Systems: Goals and Guarantees - what a type system is for, and what “checked” buys
- Subtyping, Variance, and Type Constraints - the assignability relation
Anydeliberately short-circuits - Hindley-Milner Type Inference - the inference tradition Python’s checkers borrow from without the full algorithm
- The Data Model and Dunder Methods -
__getitem__on a metaclass is what makesSequence[int]a runtime-legal expression - Decorators in Python -
functools.wrapscopies__annotations__, which is why wrapped functions keep their hints - Errors as Values vs Control Flow - where a type system can and cannot replace runtime error handling
Sources
- “PEP 484 - Type Hints,” Python Enhancement Proposals. https://peps.python.org/pep-0484/ . Supports PEP 3107 leaving annotation semantics deliberately undefined, the
greetingexample, annotations being available via__annotations__with no runtime type checking and an assumed separate off-line checker acting as a powerful linter, the mypy inspiration and the bracket notation avoiding new syntax via metaclass__getitem__, static analysis being the most important goal, the Non-goals text on runtime checking needing third party packages and performance optimization being left to the reader, the statement that Python will remain dynamically typed with no desire to make hints mandatory, and the annotation-meaning rules including@no_type_check, theAnydefault, and the implicit first-argument type for instance and class methods. - “typing - Support for type hints,” Python Standard Library. https://docs.python.org/3/library/typing.html . Supports the note that the Python runtime does not enforce annotations and that third party tools consume them, the bidirectional assignability of
Any, the worked example where assigning anAnyvalue to astrpasses checking while holding anintat runtime,Anyas an escape hatch for mixing dynamic and static code, unannotated functions defaulting toAny, theobjectcontrast, theNewTypesemantics including subclass-like treatment by checkers,int-typed results of operations, the zero-overhead callable at runtime and thesome_value is Derived(some_value)identity, andTYPE_CHECKINGbeingTruefor checkers andFalseat runtime with the expensive-import pattern and theannotationlib.get_annotations()recourse. - “PEP 649 - Deferred Evaluation Of Annotations Using Descriptors,” Python Enhancement Proposals. https://peps.python.org/pep-0649/ . Supports the original eager-evaluation semantics and the forward- and circular-reference problems they caused, PEP 563’s stringized annotations solving those while breaking runtime consumers, the Python-Version 3.14 target, the
__annotate__attribute and compiler-generated annotation function,__annotations__becoming a caching data descriptor, the delay of evaluation until annotations are examined, and theformatkeyword-only parameter added toinspect.get_annotationsandtyping.get_type_hintswithinspect.VALUE = 1as the default.