With Python 3 type annotations comes the ability to declare type Generics, which is a kind of way of making a template out of a piece of code for a specific type. For example, you can declare a Stack class that can implement a stack interface, but restrict it to only containing values of a certain type (at least with type annotations; nothing at runtime prevents multiple types from being passed). Rather than hard-coding the individual types that are allowed, a “generic” type can be declared; a variable type, if you will.
Creating a Generic variable
The typing module that comes built-in to Python 3 provides a TypeVar class that will create a Generic type variable. It is created like so:
from typing import TypeVar
T = TypeVar('T')
Using the Generic variable within a class
In order to use this generic variable within a class, the Generic class must be imported and inherited within the base class members of the class. Then, any methods can use the generic variable, T, as input values or output values:
from typing import Generic
class MyClass(Generic[T]):
def do_thing(self, items: [T]) -> Optional[T]:
for item in items:
value = item.some_method_on_t()
if some_condition(value):
return value
return None
Creating a generic base class with a concrete subclass
For my projects at work I’m using SQLAlchemy (SQLA) to interact with a database and I’ve created a base QueryDao class that wraps around SQLA’s Query class and provides business logic-specific query support while maintaining the query builder pattern of SQLA (each method returns self). The QueryDao class is a generic class that should work for any database model. As such, I have declared it like so:
from typing import Generic TypeVar, Type
from models import Base # SQLA's declarative_base class
ModelType = TypeVar("ModelType", bound=Base)
class QueryDao(Generic[ModelType]):
Model: Type[ModelType] = None
def __init__(self, session):
if self.Model is None:
raise MisconfiguredDaoException(
self, "model", "This should be set to a Model class.",
)
self._session = session
self.query = self._new_query()
self.offset = None
self.size = None
def _new_query(self):
return self._session.query(self.Model)
def build(self):
"""
Finalizes the query and returns it.
This resets the Dao to a blank state with
a new query.
"""
query = self.query
if self.size:
query = query.limit(self.size)
if self.offset:
query = query.offset(self.offset)
self.query = self._new_query()
self.reset()
return query
def get(self, entity_id, should_error=True) -> ModelType:
"""
Gets the Model by its primary key.
This uses the session cache to prevent a DB fetch.
If should_error is set, this will raise an exception
if not found (default).
"""
result = self._new_query().get(entity_id)
if not result and should_error:
raise NoResultFound("No result was found.")
return result
def all(self) -> [ModelType]:
"""
Finalizes the query and returns all rows in the
database by this query.
"""
return self.build().all()
The specific things to note here are:
- The
QueryDaoclass inherits fromGeneric[ModelType] - The
Modelclass-level attribute has a type ofType[ModelType], indicating it is a class of typeModelType - The
.get()and.all()methods returnModelTypeand[ModelType]respectively.
Now, I can subclass and supply a concrete SQLA Model class so that all of the type hints for Model, .get(), and .all() are supplied with the correct value within editors like PyCharm:
class MyModelDao(QueryDao[MyModel]):
# other specific methods here
# then, in another file where I can use the MyModelDao
dao = MyModelDao(session)
dao.get(1234) # type => MyModel
Because QueryDao inherits from Generic[ModelType], we can use indexing to provide a concrete model type for the subclass. This is done with QueryDao[MyModel]syntax. Now, any method that we use on MyModelDao that is inherited from QueryDao will show the correct return type, e.g. .get() will show a return type of MyModel which then allows us to use attributes and methods on the value with proper type-hinting!
This is great, I was trying to build something similar but got stuck. Specifically, I couldn’t figure out line 8:
Model: Type[ModelType] = Nonebut could only make it work with a class that takes a generic type AND a constructor argument.
Thank you!
Awesome, thank you for the great example. I was trying to do the same but couldn’t work out how to get the generic type into an instance attribute. So I had to pass the model type as the generic type as well as a constructor argument, which was ugly. This is way better!