Type Checking in Python

Type checking or hinting is a newer feature of Python that was added in Python 3.5. Type hinting is also known as type annotation. Type hinting is adding special syntax to functions and variable declarations that tell the developer what type the argument or variable is.

Python does not enforce the type hints. You can still change types at will in Python because of this. However some integrated development environments, such as PyCharm, support type hinting and will highlight typing errors. You can also use a tool called Mypy to check your typing for you. You will learn more about that tool later on in this article.

You will be learning about the following:

  • Pros and Cons of Type Hinting
  • Built-in Type Hinting / Variable Annotation
  • Collection Type Hinting
  • Hinting Values That Could be None
  • Type Hinting Functions
  • What To Do When Things Get Complicated
  • Classes
  • Decorators
  • Aliasing
  • Other Type Hints
  • Type Comments
  • Static Type Checking

Let’s get started!

Pros and Cons of Type Hinting

There are several things to know about up front when it comes to type hinting in Python. Let’s look at the pros of type hinting first:

  • Type hints are nice way to document your code in addition to docstrings
  • Type hints can make IDEs and linters give better feedback and better autocomplete
  • Adding type hints forces you to think about types, which may help you make good decisions during the design of your applications.

Adding type hinting isn’t all rainbows and roses though. There are some downsides:

  • The code is more verbose and arguably harder to write
  • Type hinting adds development time
  • Type hints only work in Python 3.5+. Before that, you had to use type comments
  • Type hinting can have a minor start up time penalty in code that uses it, especially if you import the typing module.

When should you use type hinting then? Here are some examples:

  • If you plan on writing short code snippets or one-off scripts, you don’t need to include type hints.
  • Beginners also don’t need to add type hints when learning Python
  • If you are designing a library for other developers to use, adding type hints may be a good idea
  • Large Python projects (i.e. thousands of lines of code) also can benefit from type hinting
  • Some core developers recommend adding type hinting if you are going to write unittests

Type hinting is a bit of a contentious topic in Python. You don’t need to use it all the time, but there are certain cases where type hinting helps.

Let’s spend the rest of this article learning how to use type hinting!

Built-in Type Hinting / Variable Annotation

You can add type hinting with the following built-in types:

  • int
  • float
  • bool
  • str
  • bytes

These can be used both in functions and in variable annotation. The concept of variable annotation was added to the Python language in 3.6. Variable annotation allows you to add type hints to variables.

Here are some examples:

x: int  # a variable named x without initialization
y: float = 1.0  # a float variable, initialized to 1.0
z: bool = False
a: str = 'Hello type hinting'

You can add a type hint to a variable without initializing it at all, as is the case in the first line of code. The other 3 lines of code show how to annotate each variable and initialize them appropriately.

Let’s see how you would add type hinting for collections next!

Sequence Type Hinting

A collection is a group of items in Python. Common collections or sequences are list, dict, tuple and set. However, you cannot annotate variables using these built-in types. Instead, you must use the typing module.

Let’s look at a few examples:

>>> from typing import List
>>> names: List[str] = ['Mike']
>>> names
['Mike']

Here you created a list with a single str in it. This specifies that you are creating a list of strings. If you know the list is always going to be the same size, you can specify each item’s type in the list:

>>> from typing import List
>>> names: List[str, str] = ['Mike', 'James']

Hinting tuples is very similar:

>>> from typing import Tuple
>>> s: Tuple[int, float, str] = (5, 3.14, 'hello')

Dictionaries are a little different in that you should hint types the key and values are:

>>> from typing import Dict
>>> d: Dict[str, int] = {'one': 1}

If you know a collection will have variable size, you can use an ellipses:

>>> from typing import Tuple
>>> t: Tuple[int, ...] = (4, 5, 6)

Now let’s learn what to do if an item is of type None!

Hinting Values That Could be None

Sometimes a value needs to be initialized as None, but when it gets set later, you want it to be something else.

For that, you can use Optional:

>>> from typing import Optional
>>> result: Optional[str] = my_function()

On the other hand, if the value can never be None, you should add an assert to your code:

>>> assert result is not None

Let’s find out how to annotate functions next!

Type Hinting Functions

Type hinting functions is similar to type hinting variables. The main difference is that you can also add a return type to a function.

Let’s take a look at an example:

def adder(x: int, y: int) -> None:
    print(f'The total of {x} + {y} = {x+y}')

This example shows you that adder() takes two arguments, x and y, and that they should both be integers. The return type is None, which you specify using the -> after the ending parentheses but before the colon.

Let’s say that you want to assign the adder() function to a variable. You can annotate the variable as a Callable like this:

from typing import Callable

def adder(x: int, y: int) -> None:
    print(f'The total of {x} + {y} = {x+y}')

a: Callable[[int, int], None] = adder

The Callable takes in a list of arguments for the function. It also allows you to specify the return type.

Let’s look at one more example where you pass in more complex arguments:

from typing import Tuple, Optional


def some_func(x: int, y: Tuple[str, str], 
              z: Optional[float]: = None): -> Optional[str]:
    if x > 10:
        return None
    return 'You called some_func'

For this example, you created some_func() that accepts 3 arguments:

  • an int
  • a two-item tuple of strings
  • an optional float that is defaulted to None

Note that when you use defaults in a function, you should add a space before and after the equals sign when using type hints.

It also returns either None or a string.

Let’s move on and discover what to do in even more complex situations!

What To Do When Things Get Complicated

You have already learned what to do when a value can be None, but what else can you do when things get complicated? For example, what do you do if the argument being passed in can be multiple different types?

For that specific use case, you can use Union:

>>> from typing import Union
>>> z: Union[str, int]

What this type hint means is that the variable, z, can be either a string or an integer.

There are also cases where a function may take in an object. If that object can be one of several different objects, then you can use Any.

x: Any = some_function()

Use Any with caution because you can’t really tell what it is that you are returning. Since it can be “any” type, it is like catching all exceptions with a bare except. You don’t know what exception you are catching with that and you also don’t know what type you are hinting at when you use Any.

Classes

If you have a class that you have written, you can create an annotation for it as well.

>>> class Test:
...     pass
... 
>>> t: Test = Test()

This can be really useful if you are passing around instances of your class between functions or methods.

Decorators

Decorators are a special beast. They are functions that take other functions and modify them. You will learn about decorators later on in this book.

Adding type hints to decorators is kind of ugly.

Let’s take a look:

>>> from typing import Any, Callable, TypeVar, cast
>>> F = TypeVar('F', bound=Callable[..., Any])
>>> def my_decorator(func: F) -> F:
        def wrapper(*args, **kwds):
            print("Calling", func)
            return func(*args, **kwds)
        return cast(F, wrapper)

A TypeVar is a way to specify a custom type. You are creating a custom Callable type that can take in any number of arguments and returns Any. Then you create a decorator and add the new type as a type hint for the first argument as well as the return type.

The cast function is used by Mypy, the static code checker utility only. It is used to cast a value to the specified type. In this case, you are casting the wrapper function as a type F.

Aliasing

You can create a new name for a type. For example, let’s rename the List type to Vector:

>>> from typing import List
>>> Vector = List[int]
>>> def some_function(a: Vector) -> None:
...     print(a)

Now Vector and List refer to the same type hint. Aliasing a type hint is useful for complex types.

The typing documentation has a good example that is reproduced below:

from typing import Dict, Tuple

ConnectionOptions = Dict[str, str]
Address = Tuple[str, int]
Server = Tuple[Address, ConnectionOptions]

This code allows you to nest types inside of other types while still being able to write appropriate type hints.

Other Type Hints

There are several other type hints that you can use as well. For example, there are generic mutable types such as MutableMapping that you might use for a custom mutable dictionary.

There is also a ContextManager type that you would use for context managers.

Check out the full documentation for all the details of all the various types:

Type Comments

Python 2.7 development ended January 1, 2020. However, there will be many lines of legacy Python 2 code that people will have to work with for years to come. Type hinting was never added to Python 2. But you can use a similar syntax as comments.

Here is an example:

def some_function(a):
    # type: str -> None
    print(a)

To make this work, you need to have the comment start with type:. This line must be on the same or following line of the code that it is hinting. If the function takes multiple arguments, then you would separate the hints with commas:

def some_function(a, b, c):
    # type: (str, int, int) -> None
    print(a)

Some Python IDEs may support type hinting in the docstring instead. PyCharm lets you do the following for example:

def some_function(a, b):
    """
    @type a: int
    @type b: float
    """

Mypy will work on the other comments, but not on these. If you are using PyCharm, you can use either form of type hinting.

If your company wants to use type hinting, you should advocate to upgrade to Python 3 to get the most out of it.

Static Type Checking

You have seen Mypy mentioned several times already. You can read all about it here:

If you would like to run Mypy on your own code, you will need to install it using pip:

$ pip install mypy

Once you have mypy installed, you can run the tool like this:

$ mypy my_program.py

Mypy will run against your code and print out any type errors that it finds. When Mypy runs, it does so without running your code. This works much like a linter does. A linter is a tool for statically checking your code for errors.

If there is no type hinting in your program, Mypy will not print report any errors at all.

Let’s write a badly type hinted function and save it to a file named bad_type_hinting.py:

# bad_type_hinting.py

def my_function(a: str, b: str) -> None:
    return a.keys() + b.keys()

Now that you have some code, you can run Mypy against it:

$ mypy bad_type_hinting.py 
bad_type_hinting.py:4: error: "str" has no attribute "keys"
Found 1 error in 1 file (checked 1 source file)

This output tells you that there is an issue on line 4. Strings do not have a keys() attribute.

Let’s update the code to remove the calls to the nonexistent keys() method. You can save these changes to a new file named bad_type_hinting2.py:

# bad_type_hinting2.py

def my_function(a: str, b: str) -> None:
    return a + b

Now you should run Mypy against your change and see if you fixed it:

$ mypy bad_type_hinting2.py 
bad_type_hinting2.py:4: error: No return value expected
Found 1 error in 1 file (checked 1 source file)

Whoops! There’s still an error. This time you know that you weren’t expecting this function to return anything. You could fix the code so that it doesn’t return anything or you could fix the type hint so that it returns a str.

You should try doing the latter and save the following code to good_type_hinting.py:

# good_type_hinting.py

def my_function(a: str, b: str) -> str:
    return a + b

Now run Mypy against this new file:

$ mypy good_type_hinting.py 
Success: no issues found in 1 source file

This time your code has no issues!

You can run Mypy against multiple files or even an entire folder. If you dedicated to using type hinting in your code, then you should be running Mypy on your code frequently to make sure your code is error free.

Wrapping Up

You now know what type hinting or annotation is and how to do it. In fact, you have learned all the basics that you need to do type hinting effectively.

In this article, you learned about:

  • Pros and Cons of Type Hinting
  • Built-in Type Hinting / Variable Annotation
  • Collection Type Hinting
  • Hinting Values That Could be None
  • Type Hinting Functions
  • What To Do When Things Get Complicated
  • Classes
  • Decorators
  • Aliasing
  • Other Type Hints
  • Type Comments
  • Static Type Checking

If you get stuck, you should check out the following resources for help:

Type hinting is not necessary in Python. You can write all your code without ever adding any annotations to your code. But type hinting is good to understand and may prove handy to have in your toolbox.