How to write mypy-safe factory for getting a class instance based on required class attributes?

294 views Asked by At

Context

I'm using ABC to create a bunch of sub-classes like so:

from abc import abstractmethod, ABC


class Person(ABC):
    @property
    @abstractmethod
    def person_name(self) -> str:
        pass


class Jason(Person):
    person_name = "jason"
    person_age = 23


class Sarah(Person):
    person_name = "sarah"
    person_age = 27

My Goal

I'm need a factory-function which will take in a person name as input and return the correct person class based on the name. I need it to be able to scale for when I write many sub-classes, as well as being mypy-safe - that is to say that mypy will raise an error if I forget to add the class attribute person_name.

What I've tried

Here is my current solution:

def person_factory(input_person_name: str) -> Person:
    if input_person_name == Jason.person_name:
        return Jason()
    elif input_person_name == Sarah.person_name:
        return Sarah()
    else:
        raise ValueError(f"Unknown Person: {input_person_name}")

This function works well because:

  • It's clear in what it's doing
  • It works with mypy. Meaning that if I create a class and forget to add the person_name class attribute, mypy will raise an error

However, I have one large issue with this function, which is that it does not scale well. At work, I will need to make a lot of these Person sub-classes, dozens, maybe hundreds in the future, and a very long chain of if/elif statements no longer looks like a good option.

So I wrote this (The type-hint on line 2 is because mypy complains otherwise):

def person_factory(input_person_name: str) -> Person:
    person_classes: List[Type[Person]] = Person.__subclasses__()

    for person_class in person_classes:
        if input_person_name == person_class.person_name:
            return person_class()

    raise ValueError(f"Unknown Person: {input_person_name}")

While this solution scales perfectly, allowing me to have any number of sub-classes, it is a little less clear (not that big of a problem for me) and, more importantly, it is not mypy-safe. If I create a class and forget to add the person_name attribute, although my IDE picks it up, mypy does not.

My Question

Is there a way to achieve my goal in Python? Am I missing something here? As this seems like a common pattern that would occur often in OOP, but I'm struggling to find anything that isn't just a bunch of if/elif, or a dictionary mapping (essentially the same thing).

Is there a way to dynamically determine the class to be created, without a large mapping object of some sorts, that will also work with static type checkers like mypy?

Update

In response to the comments in my question, I'm wondering if there's an alternative way to achieve my goal. The real-world scenario that I'm working with is that I receive a list of strings, these strings which represent various "validations" that need to be run on some data. Each validation is essentially a function, so I decided to represent these validations by creating a class for each one.

This way I could enforce a .validate() method which allows me to iterate through a list of validation class-instances and validate each one. The problem is how do I map the list of strings to the validation classes themselves? (The strings are the names of the validations, retrieved from a spreadsheet originally).

0

There are 0 answers