Reading a Python function can leave you guessing about the kind of value it expects and returns. Type hints record those expectations as annotations for readers and tools, while ordinary Python calls do not reject mismatched values automatically.

I’ll show how to annotate functions and collections, then run a static type checker separately.

TL;DR: Python type hints guide tools, not the runtime

Python type hints describe the values that code expects, but ordinary Python calls do not reject a value just because it conflicts with a hint. A static type checker can flag the mismatch before execution, while a runtime validation library must be called separately.

  • Annotate parameters with a colon and return values with an arrow.
  • Use built-in generics such as list[str] and unions such as str | None.
  • Run a type checker as a separate development step.
  • Python 3.14 defers annotation evaluation and provides annotationlib for inspection.

What are type hints and annotations in Python?

A type hint is information in an annotation that describes the kind of value a variable, parameter or return value is expected to have. Python stores annotations as metadata, and tools such as editors and static type checkers can use that metadata without changing the ordinary rules for calling a function.

That distinction answers the common question about enforcement: the interpreter does not automatically check that an argument matches its hint. A call can still pass an unexpected value, and the function body may accept it, return a different type or raise an error for its own reasons.

Mechanism When it acts What it does
Annotation Defined in source code Records expected types or other metadata
Static type checker When you run the checker or editor analysis Reports code paths that conflict with declared types
Runtime validation When validation code or a validation library runs Checks actual values during program execution

Annotations are broader than type hints. Python lets a program attach metadata to parameters, return values, variables, classes and modules, while the typing conventions give a standard meaning to many annotation values. For example, a web framework may interpret a type annotation to validate a request, but that behavior belongs to the framework, not Python’s general function-call machinery.

A static checker reasons about source and inferred types before or during development. Its finding is not a runtime guard, and a clean checker run cannot prove that external data is valid. Treat the annotation as a useful contract for people and tools, then validate untrusted input where it enters the application.

How to add and check Python type hints step by step

Function annotations make the expected input and output visible at the definition, while variable and collection annotations add detail where a value is created or passed along. The same syntax can be read by an editor and checked by a separate tool.

Step 1: Annotate a function parameter and return value

Put a colon after each parameter name and an arrow before the return type. The return annotation describes the expected result, it does not convert the result or validate it automatically.

def normalize_name(name: str | None) -> str:
    return name.strip().title() if name is not None else "Anonymous"

print(normalize_name(" ada lovelace "))
print(normalize_name(None))

from annotationlib import Format, get_annotations
print(get_annotations(normalize_name, format=Format.STRING))

Run this file with Python 3.14 using python3 annotation_demo.py. This exact run returned:

Ada Lovelace
Anonymous
{'name': 'str | None', 'return': 'str'}

I ran this file with Python 3.14 and normalize_name returned Ada Lovelace and Anonymous. annotationlib returned the parameter and return annotations as strings, including str | None for the nullable input. The function handles both branches and always returns a string.

Step 2: Describe variables and collection contents

Use the same colon syntax for a variable, and add type parameters in square brackets when the contents matter. For a dictionary, the first type describes keys and the second describes values.

names: list[str] = ["Ada", "Grace"]
ages: dict[str, int] = {"Ada": 36, "Grace": 85}
selected: str | None = names[0] if names else None

A list of strings is written list[str], while dict[str, int] describes string keys and integer values. The expression for selected can produce either a string or None, so the annotation makes the empty-list case part of the declared type.

Use built-in collection generics in current Python code, rather than the older aliases such as typing.List and typing.Dict for these basic cases. A fixed-shape tuple has a type for each position, such as tuple[int, str]. a homogeneous list normally has one element type.

Step 3: Run a static type checker separately

Choose a checker such as mypy, Pyright or another tool supported by your editor, then run it against the source files as part of development. The checker can report a call that supplies an incompatible type even though the Python interpreter itself would still attempt that call.

Type hinting also supports unions such as int | str when either type is valid. A nullable value can use str | None, and code should test for None before calling string methods.

Older Python versions differ in supported collection syntax, so match annotations to the project’s minimum version. A checker can analyze only the types it can infer from the code and its configuration. A clean result does not replace tests or validation of external data.

Common edge cases with Python annotations

Python 3.14 changes when function, class and module annotations are evaluated by default. Instead of evaluating each annotation immediately when its definition runs, Python defers evaluation until annotation values are requested, which helps with forward references and avoids doing that work at definition time.

The standard library’s annotationlib module, added in Python 3.14, provides three useful retrieval formats. VALUE requests evaluated values and can fail if a name is unresolved, FORWARDREF can preserve unresolved names as forward-reference objects, and STRING requests readable strings.

  • Python 3.13 and earlier normally evaluate annotations when the definition is executed, so a name used before it is defined can raise NameError.
  • from __future__ import annotations keeps stringized annotation behavior in Python 3.14.
  • Python 3.14’s default deferred mode can resolve a forward reference when its value is later requested, after the referenced class exists.

That change affects annotation introspection, not the core answer about type enforcement: Python still does not automatically reject a function call because its argument conflicts with a type hint. If a framework reads annotations to build validation or other behavior, the framework is adding that behavior.

Be careful when inspecting annotations from code you do not trust. The Python documentation warns that annotation introspection can execute code. APIs that evaluate annotation expressions are not a safe way to process arbitrary user-supplied strings.

typing.Annotated adds metadata alongside a type, for tools that know how to interpret it. The first argument remains the underlying type, and the extra metadata does not by itself validate a value. A tool may choose to use it, while an ordinary Python call does not gain new checks simply because the metadata is present.

For a function with a nullable parameter, a checker follows the branch that tests whether the value is None. It can then treat the value as a string inside the non-None branch, where string methods are valid. That narrowing depends on the actual condition in the code, not on Python changing the value because of its annotation.

Annotations can also describe a class parameter, an asynchronous result, or a callback, but the syntax should match the actual contract. A callback type should describe the arguments the function invokes and the value it returns. If the type becomes hard to read, a type alias can give the repeated shape a meaningful name instead of widening it to Any.

Any is available when a value is intentionally unknown to the checker, but it weakens checking for operations that use that value. Prefer a specific union or a generic type when you can state the allowed values. This keeps the annotation useful without pretending that the program validates input at runtime.

Conclusion: Treat hints as a contract for tools

Type hints state what values code expects, and a static checker can use them to find inconsistencies before execution. They remain metadata rather than automatic runtime validation, while Python 3.14’s deferred evaluation changes how annotation values are retrieved.

For reference, see the Python typing documentation, the annotationlib reference, and what’s new in Python 3.14.

FAQ

Does Python enforce type hints at runtime?

No. Ordinary Python function calls do not automatically validate arguments or return values against annotations. Use a static type checker during development or explicit runtime validation when the application needs it.

What is the difference between a type hint and an annotation?

An annotation is metadata attached to a function, variable, class or module. A type hint is an annotation used to describe an expected type for tools such as static type checkers and editors.

What changed about annotations in Python 3.14?

Python 3.14 defers evaluation of annotations by default. The annotationlib module lets code request values, forward references or strings when inspecting them.

How do you type hint a function in Python?

Write a colon and the expected type after each parameter, then use an arrow before the return type, such as def total(values: list[int]) -> int:.

Share.
Leave A Reply