What are type annotations

Python is a dynamic typed language, meaning that types are associated with the variable’s value, not the variable itself. This means that variables can take on any value at any point, and are only type checked before performing an action with them.

Type Annotations are a new feature added in PEP 484 that allow for adding type hints to variables. They are used to inform someone reading the code what the type of a variable should be. This brings a sense of statically typed control to the dynamically typed Python. This is accomplished by adding “: <type>” after initializing/declaring a variable.

We can also define the return value of a function of a method, for that we need to add “-> <type>” at the end of our function or method signature.

Last, we can expand the type annotations to variables as well, using the “: <type>” structure.

Example

Let’s start with a simple example; printing “Hello World”

def print_me(message: str) -> None:
    '''
    This function prints a message
    '''
    world_string: str = 'World'
    print(message + ' ' + world_string)

print_me('Hello')

If we run the program above, we can see the output Hello World

Problem

Let’s change a bit the program and introduce some bugs:

def print_me(message: int) -> None:
    '''
    This function prints a message
    '''
    world_string: int = 'World' # Bug: world_string is expecting an integer

    print(message + ' ' + world_string)

    return True # Bug: The function is expecting NoneType as return

print_me('Hello') # Bug: print_me is expecting and integer as argument

We should expect the program to raise errors, but if we run the program above, we get the Hello World output.

Explanation

It is important to note that yhe Python runtime does not enforce function and variable type annotations. The annotations can be used by third party tools such as type checkers, IDEs, linters.

A good static analysis tool is mypy. The description of the tool according to its authors:

Mypy is a static type checker for Python 3 and Python 2.7. If you sprinkle your code with type annotations, mypy can type check your code and find common bugs. As mypy is a static analyzer, or a lint-like tool, the type annotations are just hints for mypy and don’t interfere when running your program. You run your program with a standard Python interpreter, and the annotations are treated effectively as comments.

We can install the tool quickly with pip install mypy and running the tool:

(base) luis@luis-XPS-15-9570:~/Workspace/Python-test$ mypy test.py 
test.py:5: error: Incompatible types in assignment (expression has type "str", variable has type "int")
test.py:7: error: Unsupported operand types for + ("int" and "str")
test.py:9: error: No return value expected
test.py:11: error: Argument 1 to "print_me" has incompatible type "str"; expected "int"
Found 4 errors in 1 file (checked 1 source file)

We can see that the tool, mypy, identifies the bug introduced before (and also the concatenation bug in the print function)

Conclusion

If we want to make use of type annotations, it is important to set-up our IDE to use and highligh any errors during our coding, as well as using an static analysis tool during our CI/CD pipeline or testing scripts.

Alternative

If we want type checking during runtime, we need to assert the types in Python. For example:

def print_me(message):
    '''
    This function prints a message
    '''
    assert type(message) is str
    world_string = 'World'
    assert type(world_string) is str
    print(message + ' ' + world_string)

assert type(print_me('Hello')) is type(None)

In the example above, we will check, during runtime, the type of the arguments of the function, return and the variable defined inside the function. Of course, the resulting code is slower (as we need to do checks during runtime) and more difficult to read, therefore, my recommendation is to use the type annotations and mypy.

More information