108

Type annotations

An important part of documentation is making note of what data type our functions are expecting as input, and what they will return as output. Below is a guide for the type annotations we will be using in this course. Please follow them for all labs, assignments, and test/exam questions going forward.

Primitive types

For primitive types, we can just use their type names. The table below gives the names of the common primitive types that are built into Python. There are other built-in types that are omitted because we tend not to use them in this course.

Type name Sample values
int 0, 148, -3
float 4.53, 2.0, -3.49
str 'hello world', ''
bool True, False
None None

Note that None is a bit special, as we refer to it as both a value and its type.

Compound types

For compound types like lists, dictionaries, and tuples, we can also just use their type names: list, dict, and tuple. But often we need to be more specific. For example, often we want to say that a function takes in not just any list, but only a list of integers; we might also want to say that this function returns not just any tuple, but a tuple containing one string and one boolean value.

If we import the typing module, it provides us with a way of expressing these more detailed types. The table below shows three complex types from the typing module; the capitalized words in square brackets could be substituted with any type. Note that we use square brackets, not round ones, for these types.

Type Description Example
List[T] a list whose elements are all of type T [1, 2, 3] has type List[int]
Dict[T1, T2] a dictionary whose keys are of type T1 and whose values are of type T2 {'a': 1, 'b': 2, 'c': 3} has type Dict[str, int]
Tuple[T1, T2, ...] a tuple whose first element has type T1, second element has type T2, etc. ('hello', True, 3.4) has type Tuple[str, bool, float]

We can nest these type expressions within each other; for example, the nested list [[1, 2, 3], [-2]] has type List[List[int]].

Sometimes we want to be flexible and say that a value must be a list, but we don’t care what’s in the list (e.g. it could be a list of strings, a list of integers, a list of strings mixed with integers, etc.). In such cases, we can simply use the built-in types list, dict, and tuple for these types.

Annotating functions

Now that we know how to name the various types, let’s see how we can use this to annotate the type of a function.

Suppose we have the following function:

def can_divide(num, divisor):
        """Return whether num is evenly divisible by divisor."""
        return num % divisor == 0

This function takes in two integers and returns a boolean. We annotate the type of a function parameter by writing a colon and type after it:

def can_divide(num: int, divisor: int):

We annotate the return type of the function by writing an arrow and type after the close parenthesis, and before the final colon:

def can_divide(num: int, divisor: int) -> bool:

We can use any of the type expressions discussed above in these function type annotations, including types of lists and dictionaries. Just remember to import the typing module!

from typing import List, Tuple
    
    
    def split_numbers(numbers: List[int]) -> Tuple[List[int], List[int]]:
        """Return a tuple of lists, where the first list contains the numbers
        that are >= 0, and the second list contains the numbers that are < 0.
        """
        pos = []
        neg = []
        for n in numbers:
            if n >= 0:
                pos.append(n)
            else:
                neg.append(n)
        return pos, neg

Three misc. types

Here are three other types that you will find useful throughout the course. All four of these types are imported from the typing module.

Any

Sometimes we want to specify the that the type of a value could be anything (e.g., if we’re writing a function that takes a list of any type and returns its first element). We annotate such types using Any:

from typing import Any
    
    
    # This function could return a value of any type
    def get_first(items: list) -> Any:
        return items[0]

Warning: beginners often get lazy with their type annotations, and tend to write Any even when a more specific type annotation is appropriate. While this will cause code analysis tools (like PyCharm or pyta) to be satisfied and not report errors, overuse of Any completely defeats the purpose of type annotations! Remember that we use type annotations as a form of communication, to tell other programmers how to use our function or class. With this goal in mind, we should always prefer giving specific type annotations to convey the most information possible, and only use Any when absolutely necessary.

Union

We sometimes want to express in a type annotation that a value could be one of two different types; for example, we might say that a function can take in either an integer or a float. To do so, we use the Union type. For example, the type Union[int, float] represents the type of a value that could be either an int or a float.

from typing import Union
    
    
    def cube_root(x: Union[int, float]) -> float:
        return x ** (1/3)

Optional

One of the most common uses of a “union type” is to say that a value could be a certain type, or None. For example, we might say that a function returns an integer or None, depending on some success or failure condition. Rather than write Union[int, None], there’s a slightly shorter version from the typing module called Optional. The type expression Optional[T] is equivalent to Union[T, None] for all type expressions T. Here is an example:

from typing import Optional
    
    
    def find_pos(numbers: List[int]) -> Optional[int]:
        """Return the first positive number in the given list.
    
        Return None if no numbers are positive.
        """
        for n in numbers:
            if n > 0:
                return n