Python Function Annotations and Type Hints: A Practical, Example-Rich Guide

Introduction to Function Annotations and Type Hints in Python

When a function has several parameters, it can become difficult to tell what kind of values each parameter is expected to receive. The same problem can happen with the value returned by the function.

For example, when you see a function like calculate_total(price, quantity), you may know what the parameters mean, but the code does not clearly show what types of values they should contain.

To make this information clearer, Python provides function annotations and type hints.

Understanding function annotations and type hints in Python starts with knowing how this information can be attached to a function to describe its parameters and return value.

What Are Function Annotations and Type Hints in Python?

Function annotations and type hints in Python provide information about function parameters and return values. They can describe the expected types of values used by the function.

When these annotations are used to describe data types, they are commonly called type hints.

For example:

def calculate_total(price: float, quantity: int) -> float:
    return price * quantity

Explanation: Here,

  • float → Type hint for price.
  • int → Type hint for quantity.
  • float after -> → Type hint for the value returned by the function.

Adding Type Hints to Function Parameters

Once you know what type of values a function expects, you can add that information directly to its parameters. This makes the function definition clearer without changing how the function receives its arguments.

You can add a type hint to one parameter, several parameters, or parameters that use different data types.

Type Hint for a Single Parameter

Add a colon followed by the expected data type after the parameter name.

def greet(name: str):
    print("Hello", name)

greet("Ravi")

Explanation: Here, name: str indicates that name is expected to contain a string. The function still receives the value in the usual way when you call it.

↑ Move to Section Top

Type Hints for Multiple Parameters

You can add a type hint to each parameter when a function has multiple parameters.

def add_numbers(a: int, b: int):
    return a + b

result = add_numbers(10, 20)

print(result)


# Output:
30

Explanation: Here, a and b both have an int type hint, so both parameters are expected to receive integer values.

↑ Move to Section Top

Type Hints With Different Data Types

Each parameter can have its own type hint, so a function can have parameters with different expected data types.

def create_profile(name: str, age: int, height: float):
    print(name)
    print(age)
    print(height)

create_profile("Ravi", 25, 5.8)

Explanation: Here, name is expected to be a string, age is expected to be an integer, and height is expected to be a floating-point number.

↑ Move to Section Top

Adding a Return Type Hint to a Function

Type hints can describe function parameters, but a function can also return a value. A return type hint shows the type of value a function is expected to return.

Add a return type hint after the function’s parameter list using an arrow, ->, followed by the expected data type.

Let’s look at how return type hints are added and used in different situations.

  1. Annotating the Return Value
  2. Functions That Return No Value
  3. Matching the Return Type With the Function Result

Annotating the Return Value

For example, an integer return value can be annotated with -> int.

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

result = add_numbers(10, 20)

print(result)


# Output:
30

Explanation: Here, -> int indicates that add_numbers() is expected to return an integer. The function adds the two integer values and returns 30.

The return type hint does not automatically convert the returned value to the specified type.

↑ Move to Section Top

Functions That Return No Value

Some functions perform an action without returning a useful value. For these functions, you can use None as the return type hint.

def greet(name: str) -> None:
    print("Hello", name)

greet("Ravi")


# Output:
Hello Ravi

Explanation: Here, -> None indicates that greet() does not return a value. The function performs an action by printing a message instead.

A function without an explicit return statement returns None when it finishes.

↑ Move to Section Top

Matching the Return Type With the Function Result

The return type hint should describe the type of value the function is expected to return.

def calculate_area(length: float, width: float) -> float:
    return length * width

area = calculate_area(5.5, 3.0)

print(area)


# Output:
16.5

Explanation: Here, the calculation produces 16.5, which is a floating-point value, so -> float correctly matches the returned value.

↑ Move to Section Top

Function Annotations vs Type Hints

Function annotations and type hints in Python are closely related, but they do not mean exactly the same thing.

The main difference is that function annotation is the broader term for information attached to a function parameter or return value.

Type hint refers specifically to an annotation that describes an expected type.

Key Difference

Function Annotations Type Hints
Information added to a function’s parameters or return value. Annotations used to describe the expected data type of a value.
Can contain type information or other kinds of information. Specifically describe expected types.
The broader term. A common use of function annotations.
Example: name: str Example: name: str used to indicate that name is expected to be a string.

In short, type hints are a common type of function annotation, but function annotations are not limited to type hints.

Using Common Type Hints in Python

Python provides type hints for common data types such as int, float, str, and bool, as well as collections such as list, tuple, dict, and set.

These type hints can be used to describe the expected data type of a function parameter directly in the function definition.

  1. Type Hint for int, float, str, and bool
  2. Type Hint for list
  3. Type Hint for tuple
  4. Type Hint for dict
  5. Type Hint for set

1. Type Hint for int, float, str, and bool

Use int, float, str, and bool to describe parameters that are expected to contain integers, floating-point numbers, strings, or Boolean values.

def create_profile(name: str, age: int, height: float, active: bool):
    print(name)
    print(age)
    print(height)
    print(active)

create_profile("Ravi", 25, 5.8, True)

Explanation: Here, name: str indicates that name is expected to contain a string, age: int an integer, height: float a floating-point number, and active: bool a Boolean value.

↑ Move to Section Top

2. Type Hint for list

Use list when a parameter is expected to contain a list.

def show_items(items: list):
    print(items)

show_items(["Apple", "Banana", "Mango"])

Explanation: Here, items: list indicates that items is expected to contain a list. The list type hint does not specify the type of values the list should contain. You can add an element type when you need to describe the contents more precisely.

↑ Move to Section Top

3. Type Hint for tuple

Use tuple when a parameter is expected to contain a tuple.

def show_coordinates(point: tuple):
    print(point)

show_coordinates((10, 20))

Explanation: Here, point: tuple indicates that point is expected to contain a tuple. Like list, the tuple type hint does not specify the types of its elements.

↑ Move to Section Top

4. Type Hint for dict

Use dict when a parameter is expected to contain a dictionary.

def show_details(details: dict):
    print(details)

show_details({"name": "Ravi", "age": 25})

Explanation: Here, details: dict indicates that details is expected to contain a dictionary. The dict type hint does not specify the types of its keys or values. You can add those types when you need more specific type information.

↑ Move to Section Top

5. Type Hint for set

Use set when a parameter is expected to contain a set.

def show_numbers(numbers: set):
    print(numbers)

show_numbers({10, 20, 30})

Explanation: Here, numbers: set indicates that numbers is expected to contain a set. The set type hint does not specify the type of values stored in the set.

↑ Move to Section Top

Type Hints for Collection Elements

A type hint such as list, tuple, or dict tells you what kind of collection a function expects, but it does not tell you what types of values the collection should contain.

For example, items: list tells you that items should be a list. If the list should contain only strings, you can add the element type to the type hint.

Let’s see how to specify element types for lists, dictionaries, tuples, and nested collections.

  1. List With a Specific Element Type
  2. Dictionary With Key and Value Types
  3. Tuple With Specific Types
  4. Nested Collection Type Hints

List With a Specific Element Type

You can specify the type of values a list should contain by placing the element type inside square brackets after list.

def show_names(names: list[str]):
    print(names)

show_names(["Ravi", "Anita", "Kiran"])

Explanation: Here, list[str] indicates that names is expected to be a list containing strings.

The list describes the collection itself, while str describes the type of each element in the list.

↑ Move to Section Top

Dictionary With Key and Value Types

A dictionary contains keys and values, so you can specify the expected type for both.

def show_scores(scores: dict[str, int]):
    print(scores)

show_scores({"Ravi": 85, "Anita": 92})

Explanation: Here, dict[str, int] indicates that the dictionary is expected to have string keys and integer values.

The first type describes the keys, while the second type describes the values.

↑ Move to Section Top

Tuple With Specific Types

You can specify the types of individual elements in a tuple by listing their types inside square brackets.

def show_student(student: tuple[str, int]):
    print(student)

show_student(("Ravi", 25))

Explanation: Here, tuple[str, int] indicates that the tuple is expected to contain a string as its first element and an integer as its second element.

The order of the types matters. The first type applies to the first element, and the second type applies to the second element.

↑ Move to Section Top

Nested Collection Type Hints

Collections can contain other collections. You can include multiple type hints to describe these nested structures.

def show_students(students: list[dict[str, int]]):
    print(students)

show_students([
    {"Ravi": 25},
    {"Anita": 22}
])

Explanation: Here, list[dict[str, int]] indicates that students is expected to be a list of dictionaries.

Each dictionary is expected to have string keys and integer values. The outer list describes the collection, while dict[str, int] describes the type of each dictionary inside it.

↑ Move to Section Top

Optional Values and Union Types in Python

A parameter may allow None as a value or accept more than one data type. Python type hints let you describe these possibilities directly in the function definition.

Let’s see how to use type hints for optional values and union types.

  1. Optional Values
  2. Union Types in Python

1. Optional Values

A parameter can allow either a value of a specific type or None. You can use a union type to describe both possibilities.

def greet(name: str | None = None):
    if name is not None:
        print("Hello", name)
    else:
        print("Hello Guest")

greet("Ravi")
greet()

Explanation: Here, str | None indicates that name can contain either a string or None.

The default value None allows the function to be called without providing a value for name.

↑ Move to Section Top

2. Union Types in Python

Use a union type when a parameter can accept values of more than one data type.

def show_value(value: int | str):
    print(value)

show_value(25)
show_value("Python")

Explanation: Here, int | str indicates that value can contain either an integer or a string.

The | operator combines the allowed types into one type hint.

↑ Move to Section Top

Type Hints for Functions as Arguments

A function can receive another function as an argument. You can also add a type hint to show that a parameter is expected to receive a function.

Let’s see how functions can be passed as arguments and how to add type hints for them.

  1. What Is a Function Argument?
  2. Passing a Function as an Argument
  3. Type Hint for a Function That Accepts Another Function

1. What Is a Function Argument?

A function argument is a value passed to a function when you call it.

For example:

def greet(name):
    print("Hello", name)

greet("Ravi")

Explanation: Here, "Ravi" is the argument passed to the greet() function.

An argument does not have to be a number or string. Python also allows you to pass a function as an argument.

↑ Move to Section Top

2. Passing a Function as an Argument

You can pass a function to another function without calling it. The receiving function can then call it when needed.

def greet(name):
    return "Hello " + name

def process(func, name):
    return func(name)

result = process(greet, "Ravi")

print(result)


# Output:
Hello Ravi

Explanation: Here, greet is passed to process() as an argument. The parentheses are not used with greet because the function itself is being passed.

Inside process(), func(name) calls the function received through the func parameter.

↑ Move to Section Top

3. Type Hint for a Function That Accepts Another Function

When a function expects another function as an argument, you can use a type hint to describe the function’s parameters and return type.

Python provides Callable for this purpose.

from collections.abc import Callable

def greet(name: str) -> str:
    return "Hello " + name

def process(func: Callable[[str], str], name: str) -> str:
    return func(name)

result = process(greet, "Ravi")

print(result)


# Output:
Hello Ravi

Explanation: Here, Callable[[str], str] indicates that func is expected to be a function that accepts a string and returns a string.

The first str describes the function’s parameter type, while the second str describes its return type.

↑ Move to Section Top

Inspecting Function Annotations

Python stores function annotations as part of the function. The __annotations__ attribute lets you inspect the annotations attached to its parameters and return value.

Let’s see how to access and inspect these annotations.

  1. Using __annotations__
  2. Viewing Parameter Annotations
  3. Viewing the Return Type Annotation

1. Using __annotations__

The __annotations__ attribute contains the annotations defined for a function.

def calculate_total(price: float, quantity: int) -> float:
    return price * quantity

print(calculate_total.__annotations__)

# Output:
{'price': <class 'float'>, 'quantity': <class 'int'>, 'return': <class 'float'>}

Explanation: Here, __annotations__ returns a dictionary containing the annotations for the function parameters and return value.

The parameter names appear as keys, and the corresponding annotations appear as their values. Python uses the key "return" for the return annotation.

↑ Move to Section Top

2. Viewing Parameter Annotations

You can use __annotations__ to view the annotation attached to a specific parameter.

def greet(name: str, age: int):
    print("Hello", name)

print(greet.__annotations__["name"])
print(greet.__annotations__["age"])

# Output:
<class 'str'>
<class 'int'>

Explanation: Here, greet.__annotations__["name"] returns the annotation for the name parameter, while greet.__annotations__["age"] returns the annotation for the age parameter.

↑ Move to Section Top

3. Viewing the Return Type Annotation

The return annotation is stored under the "return" key in __annotations__.

def calculate_area(length: float, width: float) -> float:
    return length * width

print(calculate_area.__annotations__["return"])

# Output:
<class 'float'>

Explanation: Here, calculate_area.__annotations__["return"] returns the float annotation attached to the function’s return value.

↑ Move to Section Top

Type Hints With Static Type Checkers

Type hints can help static type checkers identify possible type-related problems before the program runs.

However, Python does not automatically check these type hints when the program runs. Static type checkers can analyze this type information before the program runs.

Let’s see how static type checking works, how type checkers use type hints, and how it differs from runtime type checking.

  1. What Is Static Type Checking?
  2. How a Type Checker Uses Type Hints
  3. Type Hints vs Runtime Type Checking

1. What Is Static Type Checking?

Static type checking checks the types used in your code without running the program. A static type checker reads the type hints in your code and looks for possible type-related problems, such as incorrect values passed to functions or incorrectly used function results.

For example, a function may specify that a parameter should contain an integer:

def square(number: int) -> int:
    return number * number

Explanation: Here, number: int tells a static type checker that number is expected to be an integer. The checker can use this information to identify possible type-related problems when the function is used.

↑ Move to Section Top

2. How a Type Checker Uses Type Hints

A type checker reads the type hints in a function definition and compares them with how the function is used.

For example, if a function expects an integer but receives a string, a type checker can report a possible type error before the program runs.

def square(number: int) -> int:
    return number * number

result = square("5")

Explanation: Here, number has an int type hint, but the function receives the string "5". A static type checker can flag this call because the argument does not match the expected type.

↑ Move to Section Top

3. Type Hints vs Runtime Type Checking

Type hints and runtime type checking work differently. Type hints describe the expected types, while runtime type checking checks actual values while the program is running.

For example, isinstance() can check the type of a value during program execution:

def square(number: int) -> int:
    if isinstance(number, int):
        return number * number

    return 0

Explanation: Here, number: int is a type hint, while isinstance(number, int) performs an actual type check at runtime.

↑ Move to Section Top

What Happens When the Wrong Type Hint Is Used?

A type hint describes the type of value a function is expected to receive. If the type hint does not match the value passed to the function, the type hint can become misleading.

Error Code:

Consider a function where price is given an int type hint, but a floating-point value is passed to it.

def calculate_total(price: int, quantity: int) -> int:
    return price * quantity

calculate_total(25.5, 3)

Explanation: Here, price has an int type hint, but the function receives the floating-point value 25.5. Python does not automatically enforce the type hint at runtime, so the function can still receive the value. However, the type hint does not accurately describe the value the function expects.

If the function is expected to receive a floating-point value, the type hint should be float.

Correct Code:

def calculate_total(price: float, quantity: int) -> float:
    return price * quantity

Explanation: Here, price: float correctly indicates that price is expected to be a floating-point number. The return type is also changed to float because the calculation returns a floating-point value.

Key Takeaways: Function Annotations and Type Hints

Here are the key points to remember about function annotations and type hints in Python.

  • Function annotations add information to function parameters and return values, while type hints describe the expected data types.
  • Add type hints to function parameters and return values to make the expected input and output types clear.
  • Use type hints for collections, collection elements, optional values, multiple types, and functions passed as arguments.
  • Type hints do not automatically check or convert values when a function runs.
  • Static type checkers can use type hints to find possible type-related problems before the program runs.
  • Use accurate type hints that match the values a function expects and returns.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top