I’m having an abstract class in Python like this
class Sinker(ABC):
@abstractmethod
def batch_write(self, data) -> None:
pass
The concrete class is expected to write data to some cloud databases. My intention of writing abstract class this way was to make sure the concrete classes always implement the batch_write()
method.
However, I’m not sure what type hints I should put in for data
because one of my concrete class is expecting List[str]
while another concrete class is expecting List[dict]
.
Here are just some options popped up in my head so far.
Any
List[Any]
List[str|dict]
What would be a better way to do so? Is there a style guide for this situation?
I tried out all three options and since they’re just type hints, it won’t cause me any trouble. But I just want to make sure I’m writing codes that are aligned with corresponding best practices in OOP if there’s any.
Po-Han Chen is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
2
You can make Sinker
a generic class so the type of items in the list passed to batch_write
can be parameterized:
from abc import ABC, abstractmethod
class Sinker[T](ABC):
@abstractmethod
def batch_write(self, data: list[T]) -> None:
...
class StrSinker(Sinker[str]):
def batch_write(self, data: list[str]) -> None:
pass
class DictSinker(Sinker[dict]):
def batch_write(self, data: list[dict]) -> None:
pass
Demo with Pyright
This way, the type checker would be able to spot an incorrect type hint for you. For example, with:
class StrSinker(Sinker[str]):
def batch_write(self, data: list[int]) -> None:
pass
Pyright would produce the following complaint:
Method "batch_write" overrides class "Sinker" in an incompatible manner
Parameter 2 type mismatch: base parameter is type "list[str]", override parameter is type "list[int]"
"list[str]" is incompatible with "list[int]"
Type parameter "_T@list" is invariant, but "str" is not the same as "int"
Consider switching from "list" to "Sequence" which is covariant (reportIncompatibleMethodOverride)
Demo with Pyright
I would say Any
is the recommended solution here. Since it is an abstractmethod
, people cannot directly use/implement the batch_write
method from Sinker
, and it has pass
statement, so in my opinion you don’t need to be very specific. I would implement the more specific type hints in the batch_write
method in the classes that inherit from Sinker
.
Also, if you use List[dict | str]
as type hint, then you are forced to update this type hint every time you implement a new child class which has a batch_write
method where data
has a different type (e.g. List[int]
). Just be as generic as possible in the abstract class to avoid changing the signature. But, in case you are sure it will always be a list
, you may choose for List[Any
]`. (See command under your question from Lrx, I liked that one).
4
Since the batch_write
method is not commonly defined between all the Sinker
instances, defining it ambiguously (Any
or generic) on the Sinker
won’t add any clarity about how it should be used.
People will still need to look at the subclasses to determine if their arguments are acceptable. Therefore I don’t think defining it on Sinker
adds anything.
Instead wait until the subclass where the method (including its arguments) is fully defined before making the abstractmethod, even if this involves adding another abstract intermediate eg.
class SinkerStringBatchWritable(Sinker):
@abstractmethod
def batch_write(self, data: list[str]) -> None:
pass