Recent Python releases have introduced several small improvements to the type hinting system, but Python 3.14 brings a single major change: lazy annotations. This change delays annotation evaluation until explicitly requested, improving performance and resolving issues with forward references. Library maintainers might need to adapt, but for regular Python users, this change promises a simpler and faster development experience.
By the end of this tutorial, you’ll understand that:
- Although annotations are used primarily for type hinting in Python, they support both static type checking and runtime metadata processing.
- Lazy annotations in Python 3.14 defer evaluation until needed, enhancing performance and reducing startup time.
- Lazy annotations address issues with forward references, allowing types to be defined later.
- You can access annotations via the
.__annotations__
attribute or useannotationlib.get_annotations()
andtyping.get_type_hints()
for more robust introspection. typing.Annotated
enables combining type hints with metadata, facilitating both static type checking and runtime processing.
Explore how lazy annotations in Python 3.14 streamline your development process, offering both performance benefits and enhanced code clarity. If you’re just looking for a brief overview of the key changes in 3.14, then expand the collapsible section below:
Python 3.14 introduces lazy evaluation of annotations, solving long-standing pain points with type hints. Here’s what you need to know:
- Annotations are no longer evaluated at definition time. Instead, their processing is deferred until you explicitly access them.
- Forward references work out of the box without needing string literals or
from __future__ import annotations
. - Circular imports are no longer an issue for type hints because annotations don’t trigger immediate name resolution.
- Startup performance improves, especially for modules with expensive annotation expressions.
- Standard tools, such as
typing.get_type_hints()
andinspect.get_annotations()
, still work but now benefit from the new evaluation strategy. inspect.get_annotations()
becomes deprecated in favor of the enhancedannotationlib.get_annotations()
.- You can now request annotations at runtime in alternative formats, including strings, values, and proxy objects that safely handle forward references.
These changes make type hinting faster, safer, and easier to use, mostly without breaking backward compatibility.
Get Your Code: Click here to download the free sample code that shows you how to use lazy annotations in Python 3.14.
Take the Quiz: Test your knowledge with our interactive “Python Annotations” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Python AnnotationsTest your knowledge of annotations and type hints, including how different Python versions evaluate them at runtime.
Python Annotations in a Nutshell
Before diving into what’s changed in Python 3.14 regarding annotations, it’s a good idea to review some of the terminology surrounding annotations. In the next sections, you’ll learn the difference between annotations and type hints, and review some of their most common use cases. If you’re already familiar with these concepts, then skip straight to lazy evaluation of annotations for details on how the new annotation processing works.
Annotations vs Type Hints
Arguably, type hints are the most common use case for annotations in Python today. However, annotations are a more general-purpose feature with broader applications. They’re a form of syntactic metadata that you can optionally attach to your Python functions and variables.
Although annotations can convey arbitrary information, they must follow the language’s syntax rules. In other words, you won’t be able to define an annotation representing a piece of syntactically incorrect Python code.
To be even more precise, annotations must be valid Python expressions, such as string literals, arithmetic operations, or even function calls. On the other hand, annotations can’t be simple or compound statements that aren’t expressions, like assignments or conditionals, because those might have unintended side effects.
Note: For a deeper explanation of the difference between these two constructs, check out Expression vs Statement in Python: What’s the Difference?
Python supports two flavors of annotations, as specified in PEP 3107 and PEP 526:
- Function annotations: Metadata attached to signatures of callable objects, including functions and methods—but not lambda functions, which don’t support the annotation syntax.
- Variable annotations: Metadata attached to local, nonlocal, and global variables, as well as class and instance attributes.
The syntax for function and variable annotations looks almost identical, except that functions support additional notation for specifying their return value. Below is the official syntax for both types of annotations in Python. Note that <annotation>
is a placeholder, and you don’t need the angle brackets when replacing this placeholder with the actual annotation:
Python 3.6+
class Class:
# These two could be either class or instance attributes:
attribute1: <annotation>
attribute2: <annotation> = value
def method(
self,
parameter1,
parameter2: <annotation>,
parameter3: <annotation> = default_value,
parameter4=default_value,
) -> <annotation>:
self.instance_attribute1: <annotation>
self.instance_attribute2: <annotation> = value
...
def function(
parameter1,
parameter2: <annotation>,
parameter3: <annotation> = default_value,
parameter4=default_value,
) -> <annotation>:
...
variable1: <annotation>
variable2: <annotation> = value
To annotate a variable, attribute, or function parameter, put a colon (:
) just after its name, followed by the annotation itself. Conversely, to annotate a function’s return value, place the right arrow (->
) symbol after the closing parenthesis of the parameter list. The return annotation goes between that arrow and the colon denoting the start of the function’s body.
Note: The right arrow symbol isn’t unique to Python. A few other programming languages use it as well but for different purposes. For example, Java and CoffeeScript use it to define anonymous functions, similar to Python’s lambdas. This symbol is sometimes referred to as the thin arrow (->
) to distinguish it from the fat arrow (=>
) found in JavaScript and Scala.
As shown, you can mix and match function and method parameters, including optional parameters, with or without annotations. You can also annotate a variable without assigning it a value, effectively making a declaration of an identifier that might be defined later.
Declaring a variable doesn’t allocate memory for its storage or even register it in the current namespace. Still, it can be useful for communicating the expected type to other people reading your code or a static type checker. Another common use case is instructing the Python interpreter to generate boilerplate code on your behalf, such as when working with data classes. You’ll explore these scenarios in the next section.
To give you a better idea of what Python annotations might look like in practice, below are concrete examples of syntactically correct variable annotations:
Python 3.6+
>>> temperature: float
>>> pressure: {"unit": "kPa", "min": 220, "max": 270}
You annotate the variable temperature
with float
to indicate its expected type. For the variable pressure
, you use a Python dictionary to specify the air pressure unit along with its minimum and maximum values. This kind of metadata could be used to validate the actual value at runtime, generate documentation based on the source code, or even automatically build a command-line interface for a Python script.