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.
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:
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
Here are three other 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 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.
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)
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