Python Type Hints

Introduction

Python is a dynamically typed programming language, meaning you don’t have to explicitly declare variable or function parameter types when writing code. Python infers the type, such as int, float or string, for you when assigning a value.

This flexibility lets you develop faster, since you don’t have to think of the possible valid types in advance and explicitly write them out when you code, but it can lead to runtime errors (errors that arise only when a program is run) or bugs. Additionally, as codebases grow larger and more complex, keeping track of which types of values are valid for a specific variable or argument can become more challenging.

Python type hints provide a way to make the expected types in your code explicit. They do not cause Python to enforce assignment types, but rather improve readability and enable tools such as linters or IDEs catch potential errors before runtime.

Basic Syntax of Type Hints

The basic syntax of type hints involves placing a colon : after the variable or parameter name followed by the type. For function return values, the -> symbol is used after the closing parenthesis, followed by the return type.

Type Hinting Variables

To add a type hint to a variable, write the variable name, followed by a colon and the type:

variable_name: type

Example:

age: int
name: str
height: float

In the example above, the type hints specify that age should be an integer, name a string, and height a float.

Type hints are often given in the same statement as a variable’s initial assignment.

Example:

age: int = 25
name: str = "Alice"
height: float = 5.8

In the example above, the same variables from the previous one are used to show assignments that also have type hints.

Type Hinting Function Parameters

To add a type hint to a parameters, place a colon and the type following the parameter name in the function declaration:

def function_name(param1: type, param2: type):
    # function code
    ...

Example:

def give_dog_treat(name_of_dog: str):
    return f"{name_of_dog} is getting a treat!"

In the example above, the give_dog_treat() function expects a str type for the parameter name_of_dog.

Type Hinting Function Return Values

To specify the return type of a function, place the -> symbol followed by the return type after the closing parenthesis of the parameter list:

def function_name(param1: type, param2: type) -> return_type:
    # function code
    ...

You can specify a return type without specifying parameter types and vice versa. And in fact, even though it’s best to be consistent, you can add or omit type hints in any combination you would like.

Example:

def raise_to_power(num: float, exp: float) -> float:
    return num**exp

In the example above, raise_to_power() is expected to take two floating point numbers (i.e. floats) as input and return a float.

To specify that a function does not return a value, give None as the return type, as functions that don’t explicitly use the return statement to return a value, default to returning None.

Example:

def print_a_greeting(name: str) -> None:
    print(f"Hello, {name}!")

Types You Can Use With Type Hints

The type specified by a type hint (to the right of the colon) can be the type of any Python value or object, including classes you define yourself.

If you are unsure of the name of a type you would like to specify, call the type() function (conveniently in the terminal) with a literal or object of that type as an argument. The return value of type() will tell you what it is.

Example:

print(type(10))  # --> <class 'int'>
my_number: int

print(type([1,2,3]))  # --> <class 'list'>
my_list: list

Output:

<class 'int'>
<class 'list'>

The example above demonstrates the use of the type() function to first determine the type of an integer using the literal 10 (line 1), and then the type of a list using the literal [1,2,3] (line 4). You can conveniently run type() in the terminal and not include it in your code. In the terminal, print() is unnecessary because the terminal will output the result of type(). The example also show the use of the int and list types with the my_number and my_list variable respectively.

Using Type Hints With Your Own Classes

To specify a type hint for a class you defined, simply write the name of the class after the colon, as you would any other type.

Example:

class MyClass:
    pass

my_obj: MyClass = MyClass()

The example above defines a dummy class called MyClass (line 1), and uses its type in a type hint for my_obj (line 4).

Checking Types With the MyPy Tool

Although the Python language has type hinting syntax, it does not currently provide a built-in mechanism for type checking. Fortunately, there are external, third-party tools that do so.

MyPy is a widely used type checking tool you can install with the following command (run at the terminal):

pip install mypy

After installation, you can use it to check a Python file for compliance with type hints using the following command:

mypy my_code.py

Substitute my_code.py with the name of your Python file.

If the file you give MyPy passes all checks, you’ll see a success message similar to the one below:

Success: no issues found in 1 source file

Type Checking Errors

Suppose you have the code in a file named some_math.py shown in the following example.

Example:

# some_math.py
def raise_to_power(num: float, exp: float) -> float:
    return num**exp

large_num = raise_to_power("10", "10")

The file defines the same raise_to_power() function from above (lines 2-3), and calls it with a string values, i.e. “10” and “10”, and not numbers as specified by the type hints (line 5). Therefore, if you run the command mypy some_math.py, you’ll see the following error message:

error: Argument 1 to "raise_to_power" has incompatible type "str"; expected "int"  [arg-type]
error: Argument 2 to "raise_to_power" has incompatible type "str"; expected "int"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)

Note that using integer values where floats are expected does not generate an error because an integer can be consistently converted to a float.

Variable Type Inference

Type hints for variables are not strictly necessary. Since a variable’s type can be inferred from the first value it’s assigned, type checkers can infer its type, like the Python interpreter does.

However, unlike the interpreter, type checkers such as MyPy, add a layer of protection by reporting an error when a variable is assigned a different type later on.

Example:

x = 10
x = "Hello"

In the example above, the variable x is assigned the integer 10 but in the next line, it is reassigned a value of a different type, the string “Hello”. It is perfectly legal Python, but not a great practice, which can be reported as a problem by a type checker. (This is just the simplest possible example, and in a real application, you’re likely have some other code in between those lines.)

MyPy generates the following error for a file that contains the above code:

error: Incompatible types in assignment (expression has type "str", variable has type "int")  [assignment]
Found 1 error in 1 file (checked 1 source file)

You can still benefit from specifying variable type hints because some type checkers, such as the one built into the PyCharm IDE, only report variable type errors when a hint is specified. Also, in cases where it makes sense for a variable to assume more than one type, as you’ll see in the next section.

Multi-Typed Hints

It’s possible to specify hints with one of several types.

Suppose, for example, we would like the raise_to_power() function from a previous example to also accept string parameters that will be converted to numbers by the function.

Example:

def raise_to_power(num, exp):
    return float(num)**float(exp)

In the example above, the float() function (line 2) converts values of various types to floats. A call to raise_to_power() with string arguments that represent valid numbers, proceeds as expected with no error.

Therefore, it would be great to specify type hints for raise_to_power() in a way that allows each of the parameters to accept either a float or a string. This can be accomplished with the Union object of the typing module.

To use Union, first import it from typing:

from typing import Union

Then, use the following syntax to specify a multi-type hint:

Union[type1, type2, type3, ...]

Example:

from typing import Union

def raise_to_power(num: Union[float, str], exp: Union[float, str]) -> float:
    return float(num)**float(exp)

The example above imports Union (line 1) and rewrites raise_to_power() with type hints that allow the num and exp parameters to accept strings and floats (line 3).

Pipe Notation for Multi-Type Hints

Python version 3.10 and on introduced new syntax for multi-type hints. Instead of using Union you can use a pipe character (|) to separate types following the colon of a hint.

Example:

def raise_to_power(num: float | str, exp: float | str) -> float:
    return float(num)**float(exp)

The example above rewrites raise_to_power() once again using the streamlined pipe syntax for multi-type hints (line 1).

Type Aliases

Type aliases are names you can give multi-typed hints to conveniently refer to those with a single identifier. They work for types constructed both with Union and with the | syntax.

Example:

from typing import Union

input = Union[int, str]

my_var: input = 10
my_var = '10'

The example above shows a type alias named ‘input’ that combines the int and str types (line 3). The input alias is used to define my_var as a variable that can be assigned either an integer or a string without a type checker error (lines 5-6).

The next example demonstrates the same concept with the | syntax.

Example:

input = int | str

my_var: input = 10
my_var = '10'

Type Hints for Collection Items

At this point, you know how to specify type hints for collection object, such as a list or dictionary. You simply put the collection type name after the colon in the hint. (For example, my_list: list specifies that my_list is a list.) However, you can also specify the type of items the collection has inside.

To accomplish this, first import from the typing module an object that has the same name as the Python collection type, but with the first letter capitalized. This kind of object is called a generic type.

Example:

from typing import List

The example above imports List, a generic type for a list, from typing.

Next, use the generic type in a type hint with square brackets and a specific item type inside.

Example:

from typing import List

my_list: List[int] = [1, 2, 3]

The example above, specifies that my_list is a list with int items (line 3).

You can also create type hints the same way for sets and frozen sets with the generic types Set and Frozenset, respectively.

Tuple Type Hints

For tuples, when you put one type in the brackets, the hints specifies a tuple of one or more items of that type.

Example:

from typing import Tuple

my_numbers: Tuple[int] = (4, 3, 2, 1)

In the example above, my_numbers the type hint specifies a tuple with one or more integers.

However, when you place more than one type in the brackets, the hint specifies a tuple with values of exactly those type in order.

Example:

from typing import Tuple

dog: Tuple[str, str, int] = ('Snoopy', 'Beagle', 5)

The hint in the example above specifies a tuple that has two strings and an integer in that order (line 3).

Collection Type Aliases

The typing module provides aliases that combine closely related collection types.

Mapping, for example, is an alias that combines the dict type and different dictionary variants including collections.OrderedDict, collection.defaultdict and types.MappingProxyType.

Sequence is an alias that combines types of collections that represent sequences, including lists, tuples, and ranges.

Dictionary Type Hints

For dictionaries, place two comma-separated types within the square brackets to specify the types of the key and value in that order.

Example:

from typing import Dict

my_dict: Dict[str, int] = {'one': 1, 'two': 2, 'three': 3}

In the example above, the type hint specifies that my_dict is a dictionary with string keys and integer values (line 3).

Using Built-In Collection Types Directly

Starting with Python version 3.9, you don’t have to import generic types from the typing module, and can use the square bracket notation directly with Python’s built-in collection types, e.g. list, dict, tuple, set (without the capital letter).

Example:

my_set: set[str] = {'apple', 'orange', 'banana'}

The example above shows a type hint that directly uses the collection type set to specify a set that contains strings.

The Optional Type Hint

The Optional type hint indicates a variable, parameter or function return value can either be a specific type or None. It’s equivalent to a multi-type with None as one of the types, (e.g. Optional(int) is like Union[int, None]).

Example:

from typing import Optional

dog_name: Optional[str] = None
dog_name = 'Snoopy'

In the example above, the dog_name variable has an Optional type hint that indicates it can either be a string or ‘None’ (line 3).

Example:

from typing import Optional

def greet_user(name: Optional[str] = None) -> str:
    if name is None:
        return "Hello, Guest! Please be sure to register your name."
    return f"Hello, {name}!"

In the above, additional example, the greet_user function takes an optional name parameter, which defaults to None. If a name is provided, the function returns a greeting for “Guest” with a request to register. Otherwise, it greets the user by the provided name.

Summary & Reference for Python Type Hints

Type hints explicitly specify the types of variables, function parameters, and return values in Python. They improve code readability and help catch potential errors before runtime.


To add type hints to variables, use a colon followed by the type:

age: int = 25

For function parameters, add a colon and type after the parameter name, and use the -> symbol followed by the return type for the return value.

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

A function that does not explicitly return statement has an implicitly return value of None.

def print_a_greeting(name: str) -> None:
    print(f"Hello, {name}!")

If you are unsure of the name of a type you would like to specify, use the type() function with a value of your type.

print(type(10))  # --> <class 'int'>
my_number: int

You can write a type hint for a class you define by using the class name.

class MyClass:
    pass

my_obj: MyClass = MyClass()

Use the MyPy type checker to check Python files for type compliance.

pip install mypy
mypy my_code.py

Type hints for variables are not strictly necessary. MyPy will infer a variable’s type from its first assignment and will generate an error if it’s later assigned to a different type.

x = 10
x = "Hello"  # --> Error reported by MyPy

Use Union from the typing module to specify multiple acceptable types.

from typing import Union
def process(data: Union[int, str]) -> None:
    ...

In Python 3.10 and above, use the pipe (|) symbol for multi-type hints.

def process(data: int | str) -> None:
    ...

Generic type hints for collections specify the type of collection, as well as the type of items within the collection.

from typing import List, Dict
my_list: List[int] = [1, 2, 3]
my_dict: Dict[str, int] = {'a': 1, 'b': 2}
my_tuple = Tuple[str, int, int] = ('a', 1, 2)

In Python version 3.9 and above, you and can use the square bracket notation directly with Python’s built-in collection types, instead of the generic types of the typing module.

my_dict: dict[str, int] = {'a': 1, 'b': 2}

Type aliases simplify complex type hints by giving them a single identifier.

from typing import Union
Number = Union[int, float]
my_var: Number = 10

The Optional type hint indicates that a value can be of a specific type or None.

from typing import Optional
name: Optional[str] = None