Dynamic typing of subclasses using generics in Python - python

Let's say i have to following classes.
class A:
#staticmethod
def foo():
pass
class B(A):
pass
And I have some kind of function that constructs an object based on it's type as well as calls a function.
def create(cls: Type[A]) -> A:
cls.foo()
return cls()
Now I can make the following calls to that function. And because B inherits from A it's all good.
instance_a: A = create(A)
instance_b: B = create(B)
Except the with the latter, type-checking will start complaining because create according to the annotations returns an instance of A.
This could be solved with TypeVar as follows.
from typing import Type, TypeVar
T = TypeVar('T')
def create(cls: Type[T]) -> T:
cls.foo()
return cls()
Except now the typing checking doesn't do it's original job of guarantying that cls has a method called foo. Is there a way to specify a generic to be of a certain type?

You can supply a bound:
T = TypeVar('T', bound=A)

Related

What are the correct mypy hints for this generic classmethod?

I have a series of classes that looks like this
from abc import ABC, abstractmethod
from typing import TypeVar
T = TypeVar("T", bound="A")
U = TypeVar("U", bound="ThirdPartyClass")
class ThirdPartyClass:
"""
This is from a third-party library and I don't control the implementation.
"""
#classmethod
def create(cls: type[U]) -> U:
return cls()
class A(ABC):
#classmethod
#abstractmethod
def f(cls: type[T]) -> T:
pass
class B(ThirdPartyClass, A):
#classmethod
def f(cls) -> T:
return cls.create()
When I run mypy on this module, I get two errors for the last two lines
error: A function returning TypeVar should receive at least one argument containing the same Typevar
error: Incompatible return value type (got "B", expected "T")
In my mind, neither of these are valid.
For the first one, B.f does receive an argument containing the Typevar—it receives a type[B] and since B inherits from A, and T is bound by A, type[B] is valid here.
Similarly for the second one, the return type of B should be fine because B inherits from A, and A is the bound for T.
What types should I be using here to prevent mypy from failing?

restrictive subclass type hinting

I would like typehint a function such that it only accepts a type of a subclass of Foo and returns an instance of that subclass, rather than a different subclass:
class Foo:
pass
class Bar(Foo):
pass
class Baz(Foo):
pass
class Spam:
pass
def func(t):
return t()
x: Bar = func(Bar)
y: Baz = func(Bar) # disallowed
func(Spam) # disallowed
The closest attempt I have is:
T = typing.TypeVar("T", bound=Foo)
def func(t: typing.Type[T]) -> T:
return t()
However lets say the function is more complicated and results in:
def func(t: typing.Type[T]) -> T:
return Baz()
I would like the above to error, but it doesnt
Just to basically sum up and expand a little on the comments to your question:
That proposed annotation of yours does exactly what you described:
from typing import Type, TypeVar
T = TypeVar("T", bound="Foo")
...
def func(t: Type[T]) -> T:
...
I.e. t must be the class Foo or any subclass of Foo and the output will be an instance of the class provided as an argument for t.
This means that the following should be picked up by a static type checker as wrong, regardless of what kind of class Baz is:
...
def func(t: Type[T]) -> T:
return Baz()
Returning a Baz object without consideration for t breaks the aforementioned contract.
And indeed, mypy states the following referring to the return statement:
error: Incompatible return value type (got "Baz", expected "T") [return-value]
Since this all works as expected, it is not clear what you mean, when you say this:
I would like the above to error, but it doesn't
Now, this is all relevant to static type checkers only. Python, being what it is (i.e. dynamically typed), could not care less and happily allows you to return whatever object you desire from func as well as call that function with any argument you want.
If you want errors to be raised by the Python interpreter, you will have to add some runtime type checking logic to your code yourself. For example:
...
def func(t: Type[T]) -> T:
if not isinstance(t, type) or not issubclass(t, Foo):
raise TypeError(f"{t} is not a subclass of `Foo`")
return t()

Python init object of generic type

Coming from a C# background and knowing its generic type approaches I'm now trying to implement something similar in Python. I need to serialize and de-serialize classes in a special string format, so I created the following two base classes, the first for single entity serialization and the second one for list serialization of that entity type.
from typing import Any, TypeVar, List, cast, Type, Generic, NewType
import re
T = TypeVar('T')
class Serializable(Generic[T]):
def to_str(self) -> str:
raise NotImplementedError
#classmethod
def from_str(cls, str: str):
raise NotImplementedError
class SerializableList(List[Serializable[T]]):
def __init__(self):
self.separator: str = "\n"
#classmethod
def from_str(cls, str: str):
list = cls()
for match in re.finditer(list.separator, str):
list.append(T().from_str(match)) # <-- PROBLEM: HOW TO INIT A GENERIC ENTITY ???
# list.append(Serializable[T].from_str(match)) <-- Uses base class (NotImplemented) instead of derived class
return list
def to_str(self) -> str:
str = ""
for e in self:
str = str + f"{e.to_str()}{self.separator}"
return str
Then I can derive from those classes and have to implement to_str and from_str. Please see the marker <-- PROBLEM". I have no idea how I can init a new entity of the currently used type for the list. How do we do this in the Python way?
As #user2357112supportsMonica says in the comments, typing.Generic is pretty much only there for static analysis, and has essentially no effect at runtime under nearly all circumstances. From the look of your code, it looks like what you're doing might be better suited to Abstract Base Classes (documentation here, tutorial here), which can be easily combined with Generic.
A class that has ABCMeta as its metaclass is marked as an Abstract Base Class (ABC). A subclass of an ABC cannot be instantiated unless all methods in the ABC marked with the #abstractmethod decorator have been overridden. In my suggested code below, I've explicitly added the ABCMeta metaclass to your Serializable class, and implicitly added it to your SerializableList class by having it inherit from collections.UserList instead of typing.List. (collections.UserList already has ABCMeta as its metaclass.)
Using ABCs, you could define some interfaces like this (you won't be able to instantiate these because of the abstract methods):
### ABSTRACT INTERFACES ###
from abc import ABCMeta, abstractmethod
from typing import Any, TypeVar, Type, Generic
from collections import UserList
import re
T = TypeVar('T')
class AbstractSerializable(metaclass=ABCMeta):
#abstractmethod
def to_str(self) -> str: ...
#classmethod
#abstractmethod
def from_str(cls: Type[T], string: str) -> T: ...
S = TypeVar('S', bound=AbstractSerializable)
class AbstractSerializableList(UserList[S]):
separator = '\n'
#classmethod
#property
#abstractmethod
def element_cls(cls) -> Type[S]: ...
#classmethod
def from_str(cls, string: str):
new_list = cls()
for match in re.finditer(cls.separator, string):
new_list.append(cls.element_cls.from_str(match))
return new_list
def to_str(self) -> str:
return self.separator.join(e.to_str() for e in self)
You could then provide some concrete implementations like this:
class ConcreteSerializable(AbstractSerializable):
def to_str(self) -> str:
# put your actual implementation here
#classmethod
def from_str(cls: Type[T], string: str) -> T:
# put your actual implementation here
class ConcreteSerializableList(AbstractSerializableList[ConcreteSerializable]:
# this overrides the abstract classmethod-property in the base class
element_cls = ConcreteSerializable
(By the way — I changed several of your variable names — str, list, etc — as they were shadowing builtin types and/or functions. This can often lead to annoying bugs, and even if it doesn't, is quite confusing for other people reading your code! I also cleaned up your to_str method, which can be simplified to a one-liner, and moved your separator variable to be a class variable, since it appears to be the same for all class instances and does not appear to ever be altered.)
For now I found a dirty solution - this is to add a Type (constructor) parameter of the list entries like so:
class SerializableList(List[Serializable[T]]):
# This one
# |
# v
def __init__(self, separator: str = "\n", entity_class: Type = None):
self.separator = separator
self.entity_class = entity_class
#classmethod
def from_str(cls, str: str):
list = cls()
for match in re.finditer(list.separator, str):
list.append(list.entity_class.from_str(match))
return list
I wonder if there is a cleaner way to get the correct [T] type constructor from List[T] since it is already provided there?

TypeVar in class __init__ type hinting

I am trying to use a TypeVar to indicate an init parameter as a certain type.
But I am doing it wrong, or it might not even be possible.
from typing import TypeVar
T=TypeVar("T")
class TestClass:
def __init__(self,value:T):
self._value=value
a = TestClass(value=10)
b = TestClass(value="abc")
reveal_type(a._value)
reveal_type(b._value)
I was hoping the reveal type of a._value would have been int and b._value to have been string.
But they are both revealed as 'T`-1'
Any help or insight appreciated!
[EDIT]
A little more expanded example.
The BaseClass will be overridden and the actual type hint is provided by the overriding class.
from typing import TypeVar
T=TypeVar("T")
class BaseClass:
def __init__(self,value):
self._value = value
class Class1(BaseClass):
def __init__(self,value:str):
super().__init__(value)
class Class2(BaseClass):
def __init__(self,value:int):
super().__init__(value)
a = Class1("A value")
b = Class2(10)
reveal_type(a._value)
reveal_type(b._value)
By default, using a TypeVar restricts its scope only to the method/function in which it is used as an annotation. In order to scope a TypeVar to the instance and all methods/attributes, declare the class as Generic.
from typing import TypeVar, Generic
T=TypeVar("T")
class BaseClass(Generic[T]): # Scope of `T` is the class:
def __init__(self, value: T): # Providing some `T` on `__init__`
self._value = value # defines the class' `T`
This allows declaring subclasses either as generic or as concrete.
class Class1(BaseClass[str]): # "is a" BaseClass where `T = str`
pass # No need to repeat ``__init__``
class ClassT(BaseClass[T]): # "is a" BaseClass where `T = T'`
#property
def value(self) -> T:
return self._value
reveal_type(Class1("Hello World")._value) # Revealed type is 'builtins.str*'
reveal_type(Class1(b"Uh Oh!")._value) # error: Argument 1 to "Class1" has incompatible type "bytes"; expected "str"
reveal_type(ClassT(42).value) # Revealed type is 'builtins.int*'

Subclass in type hinting

I want to allow type hinting using Python 3 to accept sub classes of a certain class. E.g.:
class A:
pass
class B(A):
pass
class C(A):
pass
def process_any_subclass_type_of_A(cls: A):
if cls == B:
# do something
elif cls == C:
# do something else
Now when typing the following code:
process_any_subclass_type_of_A(B)
I get an PyCharm IDE hint 'Expected type A, got Type[B] instead.'
How can I change type hinting here to accept any subtypes of A?
According to PEP 484 ("Expressions whose type is a subtype of a specific argument type are also accepted for that argument."), I understand that my solution (cls: A) should work?
When you specify cls: A, you're saying that cls expects an instance of type A. The type hint to specify cls as a class object for the type A (or its subtypes) uses typing.Type.
from typing import Type
def process_any_subclass_type_of_A(cls: Type[A]):
pass
From The type of class objects
:
Sometimes you want to talk about class objects that inherit from a
given class. This can be spelled as Type[C] where C is a class. In
other words, when C is the name of a class, using C to annotate an
argument declares that the argument is an instance of C (or of a
subclass of C), but using Type[C] as an argument annotation declares
that the argument is a class object deriving from C (or C itself).
If we look at the Type description from the typing module, then we see these docs:
A special construct usable to annotate class objects.
For example, suppose we have the following classes::
class User: ... # Abstract base for User classes
class BasicUser(User): ...
class ProUser(User): ...
class TeamUser(User): ...
And a function that takes a class argument that's a subclass of
User and returns an instance of the corresponding class::
U = TypeVar('U', bound=User)
def new_user(user_class: Type[U]) -> U:
user = user_class()
# (Here we could write the user object to a database)
return user
joe = new_user(BasicUser)
At this point the type checker knows that joe has type BasicUser.
Based on this, I can imagine a synthetic example that reproduces the problem with type hinting errors in PyCharm.
from typing import Type, Tuple
class BaseClass: ...
class SubClass(BaseClass): ...
class SubSubClass(SubClass): ...
def process(model_instance: BaseClass, model_class: Type[BaseClass]) -> Tuple[BaseClass, BaseClass]:
""" Accepts all of the above classes """
return model_instance, model_class()
class ProcessorA:
#staticmethod
def proc() -> Tuple[SubClass, SubClass]:
""" PyCharm will show an error
`Expected type 'tuple[SubClass, SubClass]', got 'tuple[BaseClass, BaseClass]' instead` """
return process(SubClass(), SubClass)
class ProcessorB:
#staticmethod
def proc() -> Tuple[SubSubClass, SubSubClass]:
""" PyCharm will show an error
`Expected type 'tuple[SubSubClass, SubSubClass]', got 'tuple[BaseClass, BaseClass]' instead` """
return process(SubSubClass(), SubSubClass)
But we see in docs for Type that the situation can be corrected by using TypeVar with the bound argument. Then use it in places where BaseClass is declared as a type.
from typing import TypeVar, Type, Tuple
class BaseClass: ...
B = TypeVar('B', bound=BaseClass)
class SubClass(BaseClass): ...
class SubSubClass(SubClass): ...
def process(model_instance: B, model_class: Type[B]) -> Tuple[B, B]:
""" Accepts all of the above classes """
return model_instance, model_class()
class ProcessorA:
#staticmethod
def proc() -> Tuple[SubClass, SubClass]:
return process(SubClass(), SubClass)
class ProcessorB:
#staticmethod
def proc() -> Tuple[SubSubClass, SubSubClass]:
return process(SubSubClass(), SubSubClass)
Hope this will be helpful.
Type[A] accepts also the class itself, which is not always needed.
If you want your function to accept only subclasses, you should go with NewType, like
class A:
pass
B = NewType('B', A)
def foo(cls: Type[B]):
...

Categories

Resources