How to create a Python ABC interface pattern using Pydantic

5.8k Views Asked by At

I'm implementing a Python Interface using the abstract base class (known as the strategy pattern). I want to be able to do this with Pydantic.

Without Pydantic, I would use properties, like this:

from abc import ABC,  abstractproperty

@dataclass
class PersonEntity(ABC):

    @abstractproperty
    def first_name(self):
        raise NotImplementedError

    @abstractproperty
    def last_name(self):
        raise NotImplementedError


@dataclass
class Person(PersonEntity):

    @property
    def first_name(self):
        return 'Jimmy'

    @property
    def last_name(self):
        return 'Kelly'

This way, if I were to implement another class, like

@dataclass
class SillyPerson(PersonEntity):

    @property
    def first_name(self):
        return 'Jimmy'

    @property
    def last_name(self):
        return 'Kelly'

    @property
    def sillyness(self):
        return 5

then the interface will throw an error. This helps constrain any new class that inherits from PersonEntity.

However, I want to spit this information into a FastAPI response object. I cannot do this without figuring out some kind of serializer to grab all the values of each property field, and just the property fields (which I'm struggling to do).

I would rather use Pydantic. In this case, I dont need properties, I can simply do:

from pydantic import BaseModel


class PersonEntity(ABC, BaseModel):
    first_name: str
    last_name: str


class Person(PersonEntity):
    first_name: str
    last_name: str

These will serialize in the way that I need, but I lose the interface functionality because now I have no properties, and therefore cannot use @abstractproperty.

So if I were to implement

class SillyPerson(PersonEntity):
    first_name: str
    last_name: str
    sillyness: str

there's no error, because pydantic allows this.

(Incidentally, I wasn't sure in these examples whether to inherit from BaseModel in child classes or not.)

Is there some way I can constrain the Pydantic model to give me the interface behaviour that I need, throwing errors when a field is introduced that is not included in the ABC PersonEntity class?

1

There are 1 best solutions below

0
laker93 On

In your pydantic BaseModel definition, you can configure it to forbid extra parameters.

from pydantic import BaseModel, Extra
class PersonEntity(BaseModel):
    class Config:
        extra = Extra.forbid
    first_name: str
    last_name: str

Then constructing an instance of this with extra args:

sillyperson = PersonEntity(first_name='foo', last_name='bar', sillyness='zar')

Throws an error:

pydantic.error_wrappers.ValidationError: 1 validation error for PersonEntity
sillyness
  extra fields not permitted (type=value_error.extra)

What you currently have implemented will not work since

class SillyPerson(PersonEntity):
    first_name: str
    last_name: str
    sillyness: str

defines a new dataclass (it inherits from BaseModel) with a field called 'sillyness'. It will therefore do the opposite of you want; throwing an error if an attribute called sillyness is not provided.