Skip to content

Function Annotations in Python

diagram annotations are recorded, not checked mermaid
A type annotation changes nothing about how the function runs. Python stores it and moves on -- there is no conversion, no validation and no error. The value comes from the tools that read those annotations: a type checker before you run, an editor while you type, and libraries that choose to act on them at runtime.

Unraveling Function Annotations in Python: A Comprehensive Guide

Section titled “Unraveling Function Annotations in Python: A Comprehensive Guide”

Function annotations in Python provide a way to attach metadata, including type hints, to the parameters and return values of functions. Introduced in PEP 3107 and enhanced in subsequent PEPs (Python Enhancement Proposals), function annotations offer a mechanism for developers to provide additional information about the expected types and purpose of function parameters and return values. In this comprehensive guide, we’ll explore the syntax, use cases, and best practices associated with function annotations in Python.

The syntax for function annotations involves using colons (:) after the parameter or return value name, followed by the annotation expression. Annotations can be any valid expressions, but they are commonly used for type hints.

function.py
def greet(name: str) -> str:
    return f"Hello, {name}!"

In this example, the name parameter is annotated with the type hint str, indicating that the expected type for name is a string. The return value of the function is also annotated with str, signifying that the function is expected to return a string.

Type hints, introduced in PEP 484, are a major use case for function annotations. They allow developers to indicate the expected types of function parameters and return values, providing clarity and enabling static type checking tools.

function.py
def add_numbers(a: int, b: int) -> int:
    return a + b

In this example, the add_numbers function takes two parameters, a and b, both annotated with the type hint int, and returns an int. This information is valuable for both developers and tools that perform static analysis.

Function annotations can also be combined with default parameter values:

function.py
def greet(name: str = "Guest") -> str:
    return f"Hello, {name}!"

Here, the name parameter is annotated with the type hint str, and it has a default value of “Guest.” The function is expected to return a string.

Function annotations are not limited to simple types; they can also be used for more complex types and custom classes:

function.py
from typing import List, Tuple
 
def process_data(data: List[Tuple[str, int]]) -> List[str]:
    result = [f"{name}: {score}" for name, score in data]
    return result

In this example, the data parameter is annotated with the type hint List[Tuple[str, int]], indicating that it’s expected to be a list of tuples where the first element is a string and the second element is an integer. The function is expected to return a list of strings.

When a function parameter or return value can be of any type, the None type can be used:

function.py
def print_message(message: str, times: int) -> None:
    for _ in range(times):
        print(message)

Here, the print_message function takes a message parameter of type str and a times parameter of type int. The function does not return any value (None).

Function Annotations for Multiple Return Values

Section titled “Function Annotations for Multiple Return Values”

Function annotations can also be used for functions that return multiple values:

function.py
def get_user_info() -> Tuple[str, int]:
    name = "John"
    age = 30
    return name, age

In this example, the get_user_info function returns a tuple containing the user’s name and age. The function is annotated with the type hint Tuple[str, int], indicating that it’s expected to return a tuple where the first element is a string and the second element is an integer.

In addition to the built-in types, developers can also use custom types in function annotations:

function.py
from typing import List
 
User = List[str]
def get_users() -> List[User]:
    ...

Here, we define a custom type User that is a list of strings. The get_users function is annotated with the type hint List[User], indicating that it’s expected to return a list of User objects.

The dir function can be used to access the annotations for a function:

function.py
def greet(name: str) -> str:
    return f"Hello, {name}!"
 
print(dir(greet))

Output:

command
C:\Users\Your Name> python function.py
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__original_wrapped__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__text_signature__', '__wrapped__']

In this example, we use the dir function to access the annotations for the greet function. The annotations are stored in the __annotations__ attribute, which is a dictionary containing the annotations for each parameter and the return value.

Function annotations can be used in conjunction with docstrings to provide comprehensive documentation for functions:

function.py
def greet(name: str) -> str:
    """Returns a greeting for the given name"""
    return f"Hello, {name}!"
 
print(greet.__doc__)
print(greet.__annotations__)

Output:

command
C:\Users\Your Name> python function.py
Returns a greeting for the given name
{'name': <class 'str'>, 'return': <class 'str'>}

In this example, we define a greet function that takes a name parameter of type str and returns a string. The docstring for the function provides additional information about the purpose of the function. The annotations for the function are stored in the __annotations__ attribute, which is a dictionary containing the annotations for each parameter and the return value.

  1. Use Type Hints for Readability: Type hints improve code readability and provide clear expectations for function parameters and return values.

  2. Be Consistent: Adopt a consistent style for function annotations throughout your codebase to maintain clarity.

  3. Use Descriptive Variable Names: Choose meaningful variable names and annotate them appropriately to convey the purpose of each parameter or return value.

  4. Consider Using Type Hints for Return Values: Type hints for return values can be particularly helpful for understanding the expected output of a function.

  5. Leverage Type Checking Tools: Take advantage of static type checking tools like mypy to catch potential type-related errors early in the development process.

  6. Use Type Hints with Docstrings: Combine type hints with docstrings to provide comprehensive documentation for your functions.

While function annotations offer valuable benefits, it’s essential to note that they are optional and do not enforce type checking at runtime. They primarily serve as a form of documentation and support static type checking tools.

Function annotations in Python empower developers to provide additional information about function parameters and return values, enhancing code clarity and enabling static type checking. By incorporating type hints into function annotations, developers can communicate their intentions more effectively, making it easier for both humans and tools to understand and analyze code. As you explore Python programming, consider integrating function annotations into your coding practices to improve code readability and maintainability. For more insights and practical examples, check out our tutorials on Python Central Hub!


sketch When an annotation is evaluated changed in Python 3.14 p5.js
Before 3.14, an annotation was evaluated as the def line ran, so naming a class that did not exist yet raised immediately -- which is why forward references had to be quoted. PEP 649 made evaluation lazy: the definition is accepted and the error, if any, arrives only when something reads the annotations. Both behaviours were measured on this machine.
pch.quizTag pch.quizDefaultTitle
  1. `def add(a: int, b: int) -> int`. What does `add('x', 'y')` return?

    pch.quizShowAnswer

    B — `'xy'` — Verified. The interpreter records annotations and never consults them, so the body simply concatenated the strings — returning a `str` from a function annotated `-> int`.

  2. On Python 3.14, `def f(x: NotDefinedYet)` where that name does not exist. What happens?

    pch.quizShowAnswer

    B — Accepted; NameError only when `__annotations__` is read — PEP 649 made annotation evaluation lazy in 3.14. Measured on both interpreters here: 3.11 raised at the def line, 3.14 accepted it.

  3. Why were forward references traditionally written as strings?

    pch.quizShowAnswer

    B — Because the annotation was evaluated at definition time, before the class existed — A method annotated `-> Node` inside `class Node` referred to a name that did not exist yet. Quoting deferred the evaluation; from 3.14 that is the default.

  4. What is `get_type_hints(f)` for?

    pch.quizShowAnswer

    B — Resolving string annotations into real type objects — Annotations may be stored as strings. `get_type_hints` resolves them, and `get_origin`/`get_args` then decompose a generic such as `list[int]` into `list` and `(int,)`.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading