Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

Python Annotations

Learn annotation syntax, annotated assignment statements, Python 3.14's deferred annotation model, and the basics of runtime annotation introspection.

Levels: Intermediate ~22 min read

Intermediate

Annotations let Python code associate descriptive metadata with variables, parameters, and return values. They are part of Python's language syntax; Python does not require annotations to be type hints, and annotations do not automatically enforce types.

Annotating variables

A variable annotation places a colon and an annotation expression after the variable name. An annotation can describe the intended kind of value without changing the ordinary assignment.

python
name: str = "Ada"
count: int = 3
print(name)
print(count)

Annotations sit next to the names they describe. The assignments still create ordinary Python values.

Annotated assignment statements

An annotated assignment can combine one variable or attribute annotation with an ordinary assignment. The annotation uses a colon, and a value after = is optional. Unlike ordinary assignment, the annotated-assignment syntax permits only one target.

python
count: int = 3
name: str
name = "Ada"
print(count)
print(name)

The second statement adds annotation metadata without assigning a value at that moment. A later ordinary assignment can bind the value.

Annotated assignment is different from unpacking assignment. You cannot write an annotation on several targets in one annotated-assignment statement, such as a, b: tuple[int, int] = (1, 2). Annotate each target separately when that is what you need.

python
first: int = 1
second: int = 2
print(first, second)

Where the annotation goes

A variable annotation can target a name, and annotation syntax is also allowed for a single attribute or subscription target. In Python 3.14, annotations on simple names at module or class scope are collected in a lazy annotation scope. An annotation on an attribute, subscript, or parenthesized name is not evaluated.

Inside a function, a name introduced by an annotated assignment is local to that function. Function-local annotations are not evaluated and are not stored in the function's annotation metadata.

Python 3.14 deferred annotation evaluation

PEP 649 and PEP 749 changed the default in Python 3.14: annotation expressions for functions, classes, and modules are no longer evaluated eagerly at the point of definition. Python records a way to evaluate them when their values are requested. This supports forward references without quoting names just to delay their lookup. The explicit `from __future__ import annotations` mode continues to use stringized annotations instead.

Annotating function parameters

A function parameter can have an annotation after its name. The annotation is metadata; Python does not automatically reject a different value when the function is called.

python
def repeat(text: str, times: int) -> str:
    return text * times

print(repeat("Hi", 3))

Parameter annotations describe the parameters, and the arrow introduces the return annotation.

Return annotations

A return annotation follows -> between the parameter list and the colon. It describes the function result but does not turn return values into a separately enforced type system.

python
def area(width: float, height: float) -> float:
    return width * height

print(area(4.0, 2.5))

The return annotation documents the intended result while the function still performs normal Python evaluation.

Annotations are metadata

Annotations are stored as annotation metadata on objects that support them. A function's annotations can be inspected through its annotation information. This is useful to tools and frameworks, but the language itself does not enforce the annotated types.

python
def greet(name: str) -> str:
    return f"Hello, {name}"

print(greet.__annotations__)

The annotation information records the expressions associated with the parameter and return value.

Inspecting annotations at runtime

Python 3.14 adds `annotationlib` for tools that need to inspect annotations on functions, classes, or modules. Its `get_annotations()` function accepts a format that determines how each annotation is represented. This is an advanced introspection API; ordinary functions do not need to call it just to use annotations.

Use `Format.VALUE` when evaluated annotation values are needed; it can raise an error if a name cannot be resolved. `Format.FORWARDREF` evaluates what it can and represents unresolved names with `ForwardRef` proxies. `Format.STRING` gives source-like strings, which can be helpful for documentation tools but are not guaranteed to reproduce the exact original source.

Explicit stringized annotations

The future import is a compatibility option, not a requirement for forward references under Python 3.14's default model. When it is present, annotation expressions are preserved as strings, including references to names that have not been defined.

python
from __future__ import annotations

def describe(value: NotYetDefined) -> AlsoNotDefined:
    return value

print(describe.__annotations__)

The future import preserves unresolved annotation names as strings, so this example runs without defining either name. This is the explicit compatibility mode, not Python 3.14's default deferred-value model.

Annotations do not enforce types

An annotation is not a runtime type check. If a function is annotated with int, Python does not automatically raise an error when another value is passed. Use the annotation as information for readers and tools, not as a promise enforced by the interpreter.

python
def double(value: int) -> int:
    return value * 2

print(double(3))
print(double("ha"))

The second call is valid Python because annotations alone do not enforce the annotated type.

When annotations help

Annotations are most useful when they make an interface easier to understand: function inputs and results are especially valuable places to annotate. Keep annotations accurate and readable, and remember that using annotations is separate from choosing a static type-checking tool.

Key idea

Annotations add metadata to Python names and function interfaces. They describe intent, but Python does not automatically enforce those descriptions as runtime types.

Was this page helpful?

Mark this learning resource complete to track your learning progress.