Python Typing

Daniela Lima
6 min readSep 30, 2021

--

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.

Tim’s video

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 mypyUsage:
$ 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.

Python’s newest release 3.10:

See all the new features here: https://realpython.com/python310-new-features/

In Python 3.10, you can replace Union[float, int] with the more succinct float | int. The annotation of numbers is easier to read now, and as an added bonus, you didn’t need to import anything from typing.

A special case of union types is when a variable can have either a specific type or be None. You can annotate such optional types either as Union[None, T] or, equivalently, Optional[T] for some type T. There is no new, special syntax for optional types, but you can use the new union syntax to avoid importing typing.Optional:

address: str | None

In this example, address is allowed to be either None or a string.

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 Nonedef 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 integers 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 Listx: 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 Dictx: Dict[str: str] = {"State": "Capital"}    with capital D

Set Type:

from typing import Setx: Set[str] = {"a", "b", "c"}    with capital S

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, SetVector = 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 Optionaldef 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 Anydef 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 Sequencedef 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 Tuplex: 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 intdef 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)

--

--

Daniela Lima

📚 Computer Science student. 🤖 ML/AI lover. ✍🏻 Amateur writer. 🔗 taplink.cc/limaa.ds