In the example code below, I tried to combine abstract base classes with Pydantic. I wanted to write this code because I had an existing type Ticker
which has two implementations. It can either be represented as a string or an integer value.
I later wanted to add some data classes to represent messages. It seemed intuitive to implement these using Pydantic, because these message types are closely related to some FastAPI data classes (FastAPI uses Pydantic as part of its framework).
I have my suspicions about the below design. I somewhat suspect that either the ABC
approach should be used or the BaseModel
approach should be used, and not both. This would also remove multiple inheritance from the design, which would likely be another benefit – for simplicity if nothing else.
If this is the case, I would prefer to keep the runtime type-checking behaviour offered by Pydantic. But in this case, I do not know how to implement a class Ticker
which can have two representations – as a str
and int
.
The immediate problem is a runtime error:
pydantic.errors.PydanticSchemaGenerationError: Unable to generate pydantic-core schema for <class '__main__.Ticker'>. Set `arbitrary_types_allowed=True` in the model_config to ignore this error or implement `__get_pydantic_core_schema__` on your type to fully support it.
If you got this error by calling handler(<some type>) within `__get_pydantic_core_schema__` then you likely need to call `handler.generate_schema(<some type>)` since we do not call `__get_pydantic_core_schema__` on `<some type>` otherwise to avoid infinite recursion.
For further information visit https://errors.pydantic.dev/2.8/u/schema-for-unknown-type
Example code:
from abc import ABC
from abc import abstractmethod
from pydantic import BaseModel
class AbstractTicker(ABC):
@abstractmethod
def to_str(self) -> str:
pass
@abstractmethod
def _get_value(self) -> str|int:
pass
class Ticker():
_ticker: AbstractTicker
def __init__(self, ticker: object):
if isinstance(ticker, str):
self._ticker = TickerStr(ticker)
elif isinstance(ticker, int):
self._ticker = TickerInt(ticker)
else:
raise TypeError(f'unsupported type {type(ticker)} for ticker')
def __str__(self) -> str:
return str(self._ticker)
def to_str(self) -> str:
return self._ticker.to_str()
def __eq__(self, ticker: object) -> bool:
if isinstance(ticker, Ticker):
return self._ticker._get_value() == ticker._ticker._get_value()
return False
class TickerStr(AbstractTicker, BaseModel):
_ticker_str: str
def __init__(self, ticker_str) -> None:
super().__init__()
assert isinstance(ticker_str, str), f'ticker must be of type str'
assert len(ticker_str) > 0, f'ticker cannot be empty string'
self._ticker_str = ticker_str
def __str__(self) -> str:
return f'Ticker[str]({self._ticker_str})'
def _get_value(self) -> str:
return self._ticker_str
def to_str(self) -> str:
return self._ticker_str
class TickerInt(AbstractTicker, BaseModel):
_ticker_int: int
def __init__(self, ticker_int) -> None:
super().__init__()
assert isinstance(ticker_int, int), f'ticker must be of type int'
assert ticker_int > 0, f'ticker must be > 0'
self._ticker_int = ticker_int
def __str__(self) -> str:
return f'Ticker[int]({self._ticker_int})'
def _get_value(self) -> int:
return self._ticker_int
def to_str(self) -> str:
return str(self._ticker_int)
class AbstractMessage(ABC, BaseModel):
def __init__(self) -> None:
pass
def __str__(self) -> str:
pass
class ConcreteMessage(AbstractMessage):
ticker: Ticker
def __init__(self) -> None:
super().__init__()
self.ticker = Ticker('TEST')
def __str__(self) -> str:
return f'{str(self.ticker)}'
def main():
str_ticker = TickerStr('NVDA')
int_ticker = TickerInt(1234)
print(str_ticker)
print(int_ticker)
concrete_message = ConcreteMessage()
print(concrete_message)
if __name__ == '__main__':
main()
It could be that what I have tried to write in the example code is inadvisable or there might otherwise be a reason why this is a bad idea. If this is the case, please do let me know.
1