Search code examples
pythonjsondictionarystatic-methodspython-typing

Python - static method in class creating instance of that class


I have Python data class created from JSON (quite a lot of them actually). I want to have a method for creating class instance from JSON.

I have something like this:

class FromJSONMixin:
    @staticmethod
    @abstractmethod
    def from_json(json: Union[Dict, TypedDict], **kwargs):
        raise NotImplementedError


class PatientJSON(TypedDict):
    ID: str
    Name: str
    Description: str
    BirthDate: str


@dataclass
class Patient(FromJSONMixin):
    name: str
    birth_date: str
    description: str

    @staticmethod
    def from_json(json: PatientJSON, **kwargs) -> Patient:
        return Patient(
        name=json["Name"],
        birth_date=json["BirthDate"],
        description=raw_data["Description"])

I want to create Patient objects from PatientJSON (the structure is related to the existing database, I have to integrate with it; it also does a few name-attribute translations, as you can see above). I created the FromJSONMixin to explicitly mark classes that can be created from related classes for JSONs (like PatientJSON).

Question: I get an error with the -> Patient: part, Unresolved reference 'Patient'. Why? I can't type class objects in methods in the same class? Do I have to just give up on typing the return type?


Solution

  • This is a common problems when creating modules with good type annotations. The problem is when the python interpreter is parsing the code for creating the class Patient. The return type annotation of method Patient.from_json references class Patient which is in the middle of parsing and has not yet been created. To solve this problem you usually enclose the class name in return annotation with quotes so it becomes a string. But now there is a problem with MyPy and other type checkers. they do not allow string return annotations so here is a good solution for it:

    class MyClass(SomeOtherClass):
        def __init__(self, param_a):
            self.attr_a = param_a
        
        def foo(self, bar: MyClass) -> MyClass:
            return MyClass(self.attr_a + 1)
    

    which will raise a Unresolved Reference error.

    to fix this you can enclose the method return annotation with quotes

    class MyClass(SomeOtherClass):
        def __init__(self, param_a):
            self.attr_a = param_a
        
        def foo(self, bar: 'MyClass') -> 'MyClass':
            return MyClass(self.attr_a + bar.attr_a)
    

    this will work for readability but not for type checkers such as MyPy. So for checkers such as MyPy you can make a TypeVar.

    from typing import TypeVar, Type
    
    MyClassT = TypeVar('MyClassT', bound='MyClass')
    
    class MyClass(SomeOtherClass):
        def __init__(self, param_a):
            self.attr_a = param_a
        
        def foo(self, bar: Type[MyClassT]) -> MyClassT:
            return MyClass(self.attr_a + bar.attr_a)
    

    Update 2023

    With the introduction of Self type in python version 3.11, the above example can be written as:

    from typing import Self
    
    class MyClass(SomeOtherClass):
        def __init__(self, param_a):
            self.attr_a = param_a
    
        def foo(self, bar: Self) -> Self:
            return MyClass(self.attr_a + bar.attr_a)