Type checking in Python

1. Introduction

Python is a dynamically typed language, which is also known as "Duck Typing". This means that when we declare a variable, we don't need to explicitly declare its type. Instead, the interpreter is gonna type it for us:

>>> value = 1
>>> type(value)
<class 'int'>
>>> value = 'abc'
>>> type(value)
<class 'str'>

Since Python is dynamically typed, when defining a function, we don't need to tell what is the type we expect for the arguments we need. We only know if we sent the correct type in runtime:

def add(a,b):
   return a + b

add(2,'w')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in add
TypeError: unsupported operand type(s) for +: 'int' and 'str'

This may be a hassle when we are building a big and complex project. To help us, in the Python version 3.5, it was added support for type hints.

1.1 - Function typing

To include a type hint in a function, we simply add : type after the argument declaration and -> type to define the return type:

def add(a: int,b: int) -> int:
   return a + b

1.2 - Variable typing

We add : type after the variable declaration:

a: int = 2

1.3 - Class attributes typing

We add : type after the attribute declaration:

class T:
    value: int

    def __init__(self, value: int):
        self.value = value

2 - Some of the possible types

2.1 - Primitive types

    value: int = 1
    value: float = 1.1
    value: bool = True
    value: str = 'abc'

2.2 - Generic types with type parameters

from typing import List, Tuple, Set, Dict, Optional

#we are defining a list of strings
l: List[str]  
l = ['a', 'b', 'c']


#we are defining a tuple with 3 integers and 1 string
t: Tuple[int, int, int, str] 
t = (1, 2, 3, 'a')


#we are defining a set of strings
s: Set[int] 
s = {1, 2, 3}


#we are defining a dictionary with a string as key and a float as the corresponding value
d: Dict[str, float]  
d = {'d': 6.7}


#here we are saying that the value is either a string or None
o: Optional[str] 
o = 'abc'
o = None

3.1 - Actual type checking

During runtime, our declared type is not checked, as you can see in the following example:

def add(a : str,b : str) -> str:
   return a+b
add(9,8)
>> 17
type(add(9,8))
>> <class 'int'>

The type hints are used by IDE's, libraries, type checkers, etc. Here are some examples:

3.1 - Type checking in PyCharm

Pycharm is going to give us a warning message if we assign the wrong type:

Screenshot from 2021-08-08 17-30-49.jpg

3.2 - Type checking in FastAPI

FastAPI uses type hints for parameter validation and documentation. Let's define an endpoint that receive 2 integers:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def add(a: int, b: int) -> int:
    return a + b

If we call this endpoint passing the wrong type of a parameter:

http://127.0.0.1:8000/?a=8&b=q

We are going to receive an error message:

{"detail":[{
 "loc": ["query","b"],
 "msg":"value is not a valid integer",
 "type":"type_error.integer"}]
}

4. Conclusion

While small scripts may not benefit much from type hints, bigger and more complex projects certainly do. Also it is really useful when building API's, since we need to let the client know what the endpoint is expecting to receive.