Python Typing

Annotations & Type Hints for Python 3.5 or above.

DISCLAIMER: It’s a “detailed summary”. Everything here came from Tech With Tim video about Python Typing. I’m making this post for my learning, also to store in a place I can keep checking every time I need. All credit should go to Tim and his excellent explanation.

Why do that?

It’s just to better documenting your code, to make it easier for autocomplete, and for other developers to understand what to pass. Therefore, it doesn’t change the functionality of your code. If you pass an int instead of the expected str, it won’t crash.

Static Code Analysis Tool (MYPY)

Tool to verify type mismatch — catch errors before running your code.

$ pip install mypy

Usage:
$ mypy /../Path/your_file.py

In Python, you might already know, we don’t have to define the types of our variables, functions, parameters, etc. We can just write it. However, in a lot of other programming languages that are static and strongly typed, you need to declare the type of the variable before you can assign a value to it. And then that variable can only hold that type throughout the entire program execution.

For example, in other languages, if you declare an int and give it a str, int x = “hello” , it’s going to crash.

But in Python, since it’s dynamically typed, we can just do x = 1.

Variable hint/annotation:

For x: str = 1, this is not enforcing that x store a string, it means that x should store one. Therefore, it won’t crash, because it’s simply documentation — doesn’t change the code behavior.

For x: str = 1 it returns:

error: Incompatible types in assignment (expression has type "int", variable has type "str")
Found 1 error in 1 file (checked 1 source file)

Parameter annotation:

def something(numb: float) -> int:
    return 2021 + numb

OR if you don't return anything, just use None

def person(name: str, age: int, height: float) -> None:
    print(f'{name} is {age} yo and {height} feet tall.')

The numb: float means it should receive a float and -> int should return an int.

Some advanced types:

List Type:

So a lot of times you have some, well, more advanced or more complicated types that you want to pass as an argument to a function. For example, maybe we want to have a list that has other three list in it. And these lists all have int inside them. Well, what is the type for that?

[[1, 2], [3, 4], [5, 6]]
x: list[list[int] = []

No no no... This doesn't work!

>>> TypeError: 'type' object is not subscriptable

For that, we need to import the List type from the typing module.

from typing import List

x: List[List[int] = [[1, 2], 3, 4]]    with capital L

$ mypy ../this_file.py
>>> Success: no issues found in 1 source file.

And now it's a valid type.

Dictionary Type:

Same for dictionaries, but now we just need to specify the key and the value.

from typing import Dict

x: Dict[str: str] = {"State": "Capital"}    with capital C

Set Type:

from typing import Set

x: Set[str] = {"a", "b", "c"}    with capital C

Custom Type:

This is my vector type. So now that I’ve done this, I can use vector and wherever I use vector list float will kind of be replaced there.

from typing import List, Dict, Set

Vector = List[float]

def foo(v: Vector) -> Vector:
    print(v)

Autocomplete would be: foo(v: List[float]) -> List[float]

Where there’s Vector, List[float] will replace it.

You can store a type inside a variable and use a variable as a type.

Vectors = List[Vector]    (List[List[float]])

def foo(v: Vectors) -> Vector:
    pass

Autocomplete would be: foo(v: List[List[float]]) -> List[float]

Optional Type:

def foo(output: bool=False):
    pass

Everything is fine here — no errors. However, the autocomplete (foo(). Ctrl+Space) doesn’t show the type because it’s output optional. But to make this the most correct type that it can be, you need to specify that it’s optional.

from typing import Optional

def foo(output: Optional[bool]=False):
    pass

Any Type:

This is very straightforward. But if you are willing to accept anything, then just use the any type.

from typing import Any

def foo(output: Any):
    pass

Writing this is the exact same thing as just doing def foo(output)

Because if you don’t give it a type, it’s assumed it could be anything. This is just being more explicit. You’re saying this is intended to actually accept anything.

Sequence Type:

So a lot of times when you are writing a function, in Python, you want one of the parameters to be anything that’s a sequence, you don’t care what type of sequence it is. You just want it to be a sequence.

You’ll be needing this there’s no way to specify, if the parameter should be a list or a tuple. However, if you use sequence, you’re saying that both the tuple and the list count as a sequence. And you can also specify what type the sequence should store.

from typing import Sequence

def foo(seq: Sequence[str]):
    pass

foo(("a", "b", "c"))
foo(["a", "b", "c"])
foo("hello")    ("Strings are immutable sequences of Unicode")

$ mypy ../this_file.py
>>> Success: no issues found in 1 source file.

But if you give an *int* or a *set*:

foo(1)
foo({1, 2, 3})
str: "anything that can be indexed"

$ mypy ../this_file.py
>>> error: Argument 1 to "foo" has incompatible type "int"; expected "Sequence"
Found 1 error in 1 file (checked 1 source file)

Tuple Type:

It’s a little different of the list, so what you need to do is specify what is going to be stored at every single position in the tuple.

from typing import Tuple

x: Tuple[int] = (1, 2, 3)

$ mypy ../this_file.py
>>> error: Incompatible types in assignment (expression has type "Tuple[int, int, inti]", variable ha type "Tuple[int]
Found 1 error in 1 file (checked 1 source file)

x: Tuple[int, int, int] = (1, 2, 3)
y: Tuple[int, str, int] = (5, "hello", 10)

$ mypy ../this_file.py
>>> Success: no issues found in 1 source file.

Callable Type:

This is what you use when you want to accept a function as parameter. Well, the proper type for this function would be callable. And then, in square brackets, define the parameters that the callable function is going to have, as well as the return type.

from typing import Callable

                     |parameters|return|
def foo(func: Callable[[int, int], int] -> None:
    func(1, 3)

This function has to have two *int* parameters and return an int

def add(x: int, y: int) -> int:
    return x + y

def foo(func: Callable[[int, int], int]) -> None:
    func(1, 3)

foo(add)

$ mypy ../this_file.py
>>> Success: no issues found in 1 source file.

But if we remove or add one parameter, we get an error because this function doesn't have the correct parameters inside.

To return a Callable Function:

def foo() -> Callable[[int, int], int]:
    def add(x: int, y: int) -> int:
        return x + y
return add

foo()

OR with lambda

# normal
def foo() -> Callable[[int, int], int]:
    return lambda x, y: x + y

# variables specified
def foo() -> Callable[[int, int], int]:
    func: Callable[[int, int], int] = lambda x, y: x + y
    return func

foo()

They are all fine.

For more advanced, General Type, check: https://www.youtube.com/watch?v=QORvB-_mbZ0&t=1298s (timestamp already in the link)

27