6

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?

qalis
  • 1,314
  • 1
  • 16
  • 44
  • Because of the return type annotation of `Patient.from_json`, if you change -> Patient to -> 'Patient', then this should be resolved. The problem is type annotation is trying to find the completed Patient class in the middle of creating that class. – AmaanK May 13 '21 at 07:01
  • @xcodz-dot and checkers like MyPy will be able to infer the proper class from the string, right? – qalis May 13 '21 at 07:02
  • for MyPy look at this: https://stackoverflow.com/questions/44640479/mypy-annotation-for-classmethod-returning-instance/44644576 – AmaanK May 13 '21 at 07:04
  • PD: https://stackoverflow.com/questions/61544854/from-future-import-annotations – splash58 May 13 '21 at 09:04
  • 1
    Side-note: Alternate constructors like this should be `@classmethod`s, not `@staticmethod`s, to make them useful in subclasses. Change to `@classmethod` and accepting `cls` as the first argument allows you to change `return Patient(...)` to `return cls(...)` and create the subclass when it's called on a subclass, without requiring the subclass to override it. A `TypeVar` with a `bound='Patient'` would then be used to annotate the return type (since it's not necessarily `Patient`, but it would be a subclass of `Patient`). – ShadowRanger Jun 30 '21 at 17:50

1 Answers1

7

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)
AmaanK
  • 1,032
  • 5
  • 25