Python Type Annotation
Preamble: I am currently undergoing my Masters of Data Science and I am taking two preliminary courses in Python and R. I will be documenting journey in a series of journals that I will post on Medium
In Python only an object can have a type. Variables do not have a type. Variables take on the type from the object they refer too. So this makes python variables tricky to work with. We can’t really use a variable unless we know what type it has currently (which is based on the object it refers too). We need to know this because how would we know what we can do with the variable if we do not know the type
Thus, it is good practise to document the type.
To do this, we first need to know how to name them
Primitive Types
These are some common primitive types that are in-built to Python
Type name/ Sample values
- int: 0, 2, 145, -3
- float: 4.83, 9.23, 8.66
- str: “Welcome”, ‘’
- bool: True, False
- None: None
None is a special as it is both the value and the name of the type
Compound Types
For compound types like list, dictionary and tuple — they usually go by “list”, “dict” and “tuple”. However, for compound types — we need to add an additional detail which is often the parameter.
Type:
- List: A list whose elements are of type (T) — e.g. [1, 2, 3] had type list[int]
- Dictionary: A dictionary who has type T1 and who value are of type T2 — [‘a’:1, ‘b’:1, ‘c’:3] had a type of dict[str:int]
- Tuple: A tuple who has a first element of T1 and second element of T2 etc. — [‘hello’, 5, True] has type tuple[str, int, bool]
We can also nest these type expression in each other.
E.g. Nested list of [[1, 2, 3], [-2]] has type list[list[int]].
Nested simply mean putting one type (in this case, list) inside another type
Annotating Functions:
We know how to name the types of objects/variables. Now, we learn how to annotate a type of function
Assume we have this function
def can_divide(num, divisor):
"""Return whether num is evenly divisible by divisor."""
return num % divisor == 0We can tell that this function takes in two integers and it should return a Boolean from the the docstring
Get Stephen Ayeni’s stories in your inbox
Join Medium for free to get updates from this writer.
We annotate each function parameter with this type by writing a “:” and its type afterwards
def can_divide(num:int, divisor:int):We will annotate the type of return function by writing an arrow “->” and its type after the parenthesis but before the semi-colon
def can_divide(num:int, divisor:int) -> bool:Now we can apply any of the types we listed above (Primitive and Complex Types) when annotating a function
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, negAnnotating Instance Attributes
To annotate the instance of a class attribute, we list each attribute along with its type in the body of the class. By convention, we list this at the top of the class after its docstring before any method
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 omittedAnnotating Methods
Annotating the Method is quite similar to annotating any function except for two minor changes:
- We do not annotate the first parameter (self) — this is assumed to be the class of the method by convention
- Sometimes we need to refer to the class itself, because it is the type of some other parameter or because it is the type of the return function. By Python process, this requires us to include a special import statement at the top of our Python file.
# 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:
...Four Advanced Types
They are four more advanced types that you will most likely use:
Two uses special syntax “|” and the other are imported from built-in typing module
Two or more possible types:
Sometimes, we might want to state that a value could take on two or more types. We simply do this by applying the “|” and the types before and after the straight line. The straight line is called “union”
Example: “float|int” mean that the value could take on float or integer type
def cube_root(x: int | float) -> float:
return x ** (1 / 3)A type or None:
One of the most use cases of “|” is to say that the value can be a type or None. So it is written in this formate “<t>| None” where “<t>” represent any type. This is often used when a value can be a type or None depending on the success or failure condition
def find_pos(numbers: list[int]) -> int | None:
"""Return the first positive number in the given list.
Return None if no numbers are positive.
"""
for n in numbers:
if n > 0:
return nAny:
Something we want to specify that the value can be anything.
Example: if we are writing a function that takes a list (this could be a list of any type of: Int, bool, str, float etc.) and returns its first element. We will “Any” in this use case and it is imported from “typing” module
from typing import Any
# This function could return a value of any type
def get_first(items: list) -> Any:
return items[0]Important Note: Some developer abuse the “Any” annotations and use it all the time. This is fruitless. The point of annotating is to communicate as much details as possible to the other programmers. So also choose to go as detailed as possible with your annotations with data types and only use “Any” when absolutely necessary
Important Note 2: Many Python checkers assume “None” is included in the data type for “Any”. However some don’t. So for the sake of learning, we will assume that “Any” is strictly for non-”None” types and so when we want to say “Any” and “None”- we will use “Any|None”
Callable:
What happens when the parameter, return value or instance attribute is a function itself?
This is where we use Callable. How?
- First get
typingto importCallable - This type has two expressions: 1st is a list of type representing the functions argument, 2nd is the return type
Example:
The type Callable[[int, str], bool]
- Argument for the function that takes on two expression: Integer and String
- Return Function has a type of 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








