TIL - List from Typing module is inheritable
The "Today I learned" series of posts are short notes of new things I encounter with Python. This post discusses some under the hood behavior of the List class from the typing module.
While browsing the source of SQLAlchemy, I came across a custom list class.
The custom class is created by inheriting from List
from typing
, rather than the builtin list
or the UserList
class provided by collections
It works.
from typing import List
class CustomList(List):
pass
print(CustomList.__mro__)
# (<class '__main__.CustomList'>, <class 'list'>, <class 'typing.Generic'>, <class 'object'>)
The builtin list
can be seen as a parent class of CustomList.
Normal list operations also work as expected.
from typing import List
class CustomList(List):
pass
c = CustomList()
c.append("foo")
print(c)
# ['foo']
Now, what if we try to instantiate List
directly?
list_obj1 = list() # Works
list_obj2 = List() # TypeError: Type List cannot be instantiated; use list() instead
Let's see why this happens.
The List
class is defined in typing as an alias of the list
class.
# CPython - typing.py (https://github.com/python/cpython/blob/4afa7be32da32fac2a2bcde4b881db174e81240c/Lib/typing.py#L2609)
List = _alias(list, 1, inst=False, name='List')
This _alias
creates List
as a child instance of _BaseGenericAlias class
. See that the inst
attribute is set to False above.
Looking at the source code of _BaseGenericAlias class:
# CPython - typing.py (https://github.com/python/cpython/blob/4afa7be32da32fac2a2bcde4b881db174e81240c/Lib/typing.py#L1107)
class _BaseGenericAlias(_Final, _root=True):
"""The central part of internal API.
This represents a generic version of type 'origin' with type arguments 'params'.
There are two kind of these aliases: user defined and special. The special ones
are wrappers around builtin collections and ABCs in collections.abc. These must
have 'name' always set. If 'inst' is False, then the alias can't be instantiated,
this is used by e.g. typing.List and typing.Dict.
"""
def __init__(self, origin, *, inst=True, name=None):
self._inst = inst
self._name = name
self.__origin__ = origin
self.__slots__ = None # This is not documented.
def __call__(self, *args, **kwargs):
if not self._inst:
raise TypeError(f"Type {self._name} cannot be instantiated; "
f"use {self.__origin__.__name__}() instead")
...
The call dunder is overridden to raise a type error if the _inst
attribute of the alias is False. The __call__
method gets invoked when we call the children of the _BaseGenericAlias
class, in our case, the List
.
As you can see in the docstring of _BaseGenericAlias
:
If 'inst' is False, then the alias can't be instantiated, this is used by e.g. typing.List and typing.Dict.
Summarizing,
typing.List
behaves like alist
when used for inheritance,But it can't be used directly just like the
list
for instantiation.