Lately I have been thinking about type hints a lot because I like to be as clear as possible when writing code.

I was recently working alongside someone who wrote a function that could return objects of different types. A lot of procedural code was written, and I was unfamiliar with the code and what it was for.

My first step: identify what objects are being returned, and make it obvious for whoever comes after me. Let’s go over an example.

Here is a simple function with type hints:

from typing import Sequence, Union

def f(*args: Sequence) -> Union[bool, int]:
  '''Returns the length of `args` or returns False if none provided.'''
  if args:
      return len(args)
  if not args:
    return False

if __name__ == '__main__':
    lengths: list = [f(*('a', 'b', 'c')), f()]
    for l in lengths:
      if isinstance(l, int):
        print(f'The length of the arguments passed to `f` is: {l}.')
      else:
        print(f'No arguments were passed.')

f is a function that returns one of two things: * If it is called with an argument, it returns the length of that argument. * If it is called without an argument, it returns False.

This function isn’t specifically meaningful, nor is it a way that I think people should be writing their functions. It is just an example of how to use the typing module.

At the top of our file we have from typing import Sequence, Union. This sets us up to use these types for our annotations later. f accepts one parameter, *args, which is meant to take a variable amount of arguments, and *args is clearly meant to be a Sequence type. * For a refresher on Sequence types, check out the Python documentation here: https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range

f also has an annotation for the return type, which is what the -> Union[bool, int] is for. Union[bool, int] indicates that f can return either a bool or a int.

You can have type annotations for your variables too, lengths: list = ....

The result of running the above code is that the two following strings get printed:

The length of the arguments passed to `f` is: 3.
The length of the arguments passed to `f` is: False.

Do you need type hints to write good Python code? Not really. You would get the same result if you removed all the annotations. So why use it?

For the scenario where you are new to a code base and functions can return all sorts of different types, function type annotations can help you understand what is happening. Another good reason is for the sake of linting. I use Visual Studio Code with the Pylance linter.

If I have the following example:

def g(arg: list) -> bool:
    return True if isinstance(arg, list) else False

ex_dict = {'a': 1, 'b': 2, 'c': 3}
g(ex_dict)

Pylance will let me know that I have a problem here:

(variable) ex_dict: dict[str, int]
Argument of type "dict[str, int]" cannot be assigned to parameter "arg" of type "list[Unknown]" in function "g"
  "dict[str, int]" is incompatible with "list[Unknown]" Pylance(reportGeneralTypeIssues)

Now I can catch my own mistakes as I write them.

If you turn on type checking rules in Pylance, you’ll that it doesn’t like the annotation for our first example either. That’s okay and you can choose to ignore that warning by appending # type: ignore to the end of the offending line. The first example is still “legal”, and from my understanding, type checking isn’t always perfect, as can be seen in this issue thread: https://github.com/microsoft/pylance-release/issues/822

A “perfect example” would be the following:

from typing import Union
def h(x: Union[int, bool]) -> Union[int, bool]:
    return x

You wouldn’t get any complaints from Pylance for this example. Annotations are helpful, but not enforced by Python at all, it is just for our own readability.

If you wanted to write code that acted on the annotations you are using, you can access them as a dict object through h.__annotations__ for example.

>>> from typing import Union
>>> def h(x: Union[int, bool]) -> Union[int, bool]:
...     return x
...
>>> h.__annotations__
{'x': typing.Union[int, bool], 'return': typing.Union[int, bool]}

You can learn more about annotations from PEP 3107.

I hope you found this useful. As always, thanks for reading.

Happy programming!