rogulski.it

My name is Piotr, a passionate pythonista and this is my blog!

Python type annotation improvement with Generic and TypeVar

Posted at — Oct 15, 2021
Python typing with Generic and TypeVar

In this article, I would like to share my favorite way of type annotating classes using typing modules with Generic and TypeVar.

Checker installation

First, to check python typing you can use mypy library or any IDE/editor with type checker like Pycharm.

pip install mypy

Generic

It is an abstract base class for generic types. A generic type is typically declared by inheriting from an instantiation of this class with one or more type variables

TypeVar

It is a type variable. Type variables exist primarily for the benefit of static type checkers. They serve as the parameters for generic types as well as for generic function definitions.

Showcase

As an example, we will build a base class for our database repository, and then we will use it.

First, let’s define our base repository:

import abc

from schemas.base import BaseSchema
from tables.base import BaseTable


class BaseRepository(metaclass=abc.ABCMeta):
    @property
    @abc.abstractmethod
    def _schema(self) -> Type[BaseSchema]:
        ...
    
    @property
    @abc.abstractmethod
    def _table(self) -> Type[BaseTable]:
        ...
    
    def create(self, schema: BaseSchema) -> Type[BaseTable]:
        table = self._table(**schema.__dict__)
        return table

Now, let’s build the test repository:

from schemas.base import BaseSchema
from tables.base import BaseTable


@dataclass()
class UserSchema(BaseSchema):
    id: int
    name: str


@dataclass()
class UserTable(BaseTable):
    id: int
    name: str


class UserRepository(BaseRepository):
    @property
    def _table(self) -> UserTable:
        return UserTable

    @property
    def _schema(self) -> UserSchema:
        return UserSchema

    def perform_create(self) -> UserTable:
        schema = UserSchema(id=1, name="Piotr")
        return self.create(schema)

The problem with the above code is that every time we want to run the create method it will return the BaseTable type. You will see the error:

Expected type 'UserTable', got 'BaseTable' instead

We can fix this by introducing TypeVar with Generic:

import abc 
from typing import TypeVar, Generic

from schemas.base import BaseSchema
from tables.base import BaseTable


SCHEMA = TypeVar("SCHEMA", bound=BaseSchema)
TABLE = TypeVar("TABLE", bound=BaseTable)


class BaseRepository(Generic[SCHEMA, TABLE], metaclass=abc.ABCMeta):
    @property
    @abc.abstractmethod
    def _schema(self) -> Type[SCHEMA]:
        ...
    
    @property
    @abc.abstractmethod
    def _table(self) -> Type[TABLE]:
        ...
    
    def create(self, schema: SCHEMA) -> TABLE:
        table = self._table(**schema.__dict__)
        return table

New implementation of the UserRepository should inherit now from the base with the definition of the types we want to use:

from schemas.base import BaseSchema
from tables.base import BaseTable


@dataclass()
class UserSchema(BaseSchema):
    id: int
    name: str


@dataclass()
class UserTable(BaseTable):
    id: int
    name: str


class UserRepository(BaseRepository[UserSchema, UserTable]):
    @property
    def _table(self) -> UserTable:
        return UserTable

    @property
    def _schema(self) -> UserSchema:
        return UserSchema

    def perform_create(self) -> UserTable:
        schema = UserSchema(id=1, name="Piotr")
        return self.create(schema)

Introducing the above change we are eliminating the error and making code cleaner and better type hinted.

Summary

This is one of many cases where this approach can be used. Feel free to experiment and play on your own. I have already used it in my previous tutorial on the example with SQLAlchemy, FastAPI, and Pydantic, check it on my github.