In many programming languages, we cannot use a variable until we have declared its type, which determines the values that can be assigned to it; furthermore, a variable’s type can never change. Python takes a very different approach: only objects have a type, not the variables that refer to those objects; and in fact, a variable can refer to any type of object. Nonetheless, we can’t use a Python variable unless we know what type of object it refers to at the moment—how would we know what we can do with it?
Since we need to be aware of the types we are using at any point in our code, it is good practise to document this. In this course, we will document the types of all functions and class instance attributes. We’ll use Python’s relatively new type annotation syntax to do so.
Before we can begin documenting types, we need to learn how to name them.
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.
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.
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:
We annotate the return type of the function by writing an arrow and type after the close parenthesis, and before the final colon:
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
To annotate the instance attributes of a class, we list each attribute along with its type directly in the body of the class. By convention, we usually list these at the very top of the class, after the class docstring and before any methods.
from typing import Dict, Tuple
class Inventory:
"""The inventory of a store.
Keeps track of all of the items available for sale in the store.
Attributes:
size: the total number of items available for sale.
items: a dictionary mapping an id number to a tuple with the
item's description and number in stock.
"""
size: int
items: Dict[int, Tuple[str, int]]
... # Methods omitted
Annotating the methods of a class is the same as annotating any other function, with two notable exceptions:
self
. Its type is always understood to be the class that this method belongs to.Here is an example (for brevity, method bodies are omitted):
# This is the special import we need for class type annotations.
from __future__ import annotations
class Inventory:
# The type of self is omitted.
def __init__(self) -> None:
...
def add_item(self, item: str, quantity: int) -> None:
...
def get_stock(self, item: str) -> int:
...
def compare(self, other: Inventory) -> bool:
...
def copy(self) -> Inventory:
...
def merge(self, others: List[Inventory]) -> None:
...
Here are four more advanced types that you will find useful throughout the course. All four of these types are imported from the typing
module.
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 python_ta
) 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.
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
.
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
Finally, we sometimes need to express that the type of a parameter, return value, or instance attribute is itself a function. To do so, we use the Callable
type from the typing
module. This type takes two expressions in square brackets: the first is a list of types, representing the types of the function’s arguments; the second is its return type. For example, the type Callable[[int, str], bool]
is a type expression for a function that takes two arguments, an integer and a string, and returns a boolean. Below, the type annotation for compare_nums
declares that it can take any function that takes two integers and returns a boolean:
from typing import Callable
def compare_nums(num1: int, num2: int,
comp: Callable[[int, int], bool]) -> int:
if comp(num1, num2):
return num1
else:
return num2
def is_twice_as_big(num1: int, num2: int) -> bool:
return num1 >= 2 * num2
>>> compare_nums(10, 3, is_twice_as_big)
10
>>> compare_nums(10, 6, is_twice_as_big)
6