2. Advanced Type Hints


We saw how to annotate variables with basic data types such as int, str, bool, etc. However, in real-world applications, we work with advanced data structures like lists, tuples, dictionaries, and many more. To annotate variables with these Data structures we cannot use them directly up until Python 3.9. Python introduced the typing module for these applications.

They can be best understood using examples. Let's review the most common ones:

1.  Annotating with simple data structures.

# For python version <=3.9
from typing import List, Tuple, Dict 


price: List[int] = [213,234,984]
immutable_price: Tuple[int,int,int] = (231,983,704)
price_dict: Dict[str,int] = {
    'item_1' : 340,
    'item_2' : 500,
}

In the above snippet, we saw, how to annotate with a type of basic data structure. However, this is valid for Python 3.9 and lower. In the newer versions of Python, we can directly use the list, tuple, and dict keywords.

new_price: list[int] = [14,902,898]
new_immutable_price: tuple[int,int,int] = (388,392,299)
new_price_dict: dict[str,int] = {
    "item_1":240,
    "item_2":490,
}

2. Annotating with complex data structures
⚠️ For now, I am proceeding with the older and established syntax for type annotations. Python does support a newer way to write type hints but there are some issues with it. So, as of now stick with the older syntax.

from typing import Union

x: List[Union[int,float]] = [2,3,4.1,5,6.2]

# x: List[int|float] = [2,3,4.1,5,6.2]  #newer syntax in python 3.10+

Notice, how we used the Union keyword in the above example to show that the list x can accommodate both integers as well as float. Let's see a very common pattern, Many times, we either return a value or None from our function. To document such situations, we can use Union.

def inr_to_usd(value:float) -> Union[float,None]:
    try:
        conversion_factor = 75
        value = value/conversion_factor
        return value
    except TypeError:
        return None

inr_to_usd('23')

If we try to call the inr_to_usd function with a string value and run a static type checker like mypy, It would tell us:

(env) C:\v2\basics\type_hints> mypy .\type_hints.py
1_type_hints.py:41: error: Argument 1 to "inr_to_usd" has incompatible type "str"; expected "float"
Found 1 error in 1 file (checked 1 source file)

There is a shortcut way though for this pattern. We could have written Optional[float] which is exactly the same as Union[float,None].

3. Custom Types:
If your type hints are becoming too lengthy, then it becomes very difficult to read and understand the function. In such cases, we can extract the type annotation and build our own custom type. Let's see an example of this.

from typing import List 

Image = List[List[int]]

def flatten_image(image: Image)->List:  #custom type Image
    flat_list = []
    for sublist in image:
        for item in sublist:
            flat_list.append(item)
    return flat_list

image = [[1,2,3],[4,5,6]]

Notice how we extracted out the list of integers and build our own custom type named Image!
Isn't it beautiful and clean? But that's not it, we can type annotate our custom classes too! Let's give it a try.

from typing import Optional

class Job:
    def __init__(self,title:str,description:Optional[str]) -> None:
        self.title = title
        self.description = description

    def __repr__(self):
        return self.title


job1 = Job(title="SDE2",description="Sdfdk")
job2 = Job(title="Senior Manager", description="jfjdj")

jobs: List[Job] = [job1,job2]     #notice the List[Job] , Job is our custom class

4.  Annotate functions with callables
I personally use decorator pattern a lot. A decorator is basically a function that wraps another function and adds additional functionality to it. This is similar to packing a gift. The decorator acts as a wrapper. Below, we have a simple divide function but, division by 0 is not practical. What we can do is decorate the divide function with another function that can handle the edge case of division by 0.

def smart_divide(func):
    def inner(a, b):
        if b == 0:
            print("Whoops! Division by 0")
            return None

        return func(a, b)
    return inner


@smart_divide
def divide(a, b):
    print(a/b)

divide(9, 0)

If we want to annotate the func parameter then we can make use of the Callable class. This will help other developers understand the signature of the func parameter.
 

from typing import Callable

def smart_divide(func:Callable[[int,int],float]):
    def inner(a, b):
        if b == 0:
            print("Whoops! Division by 0")
            return None

        return func(a, b)
    return inner

The part Callable[[int,int],float] means that the func parameter expects 2 integer values, and returns 1 floating point number.

There are many more topics in type hinting, for example, generics, typevar, and much more. However, I have tried to cover the most common patterns and they should be sufficient for 99% of the cases.

Prev: 1. Type Hints … Next: 3. Generators in …
FastAPITutorial

Brige the gap between Tutorial hell and Industry. We want to bring in the culture of Clean Code, Test Driven Development.

We know, we might make it hard for you but definitely worth the efforts.

Contacts

Refunds:

Refund Policy
Social

Follow us on our social media channels to stay updated.

© Copyright 2022-23 Team FastAPITutorial