pythoninheritancetype-hintingpython-dataclassesabstract-base-class

Access Type Hints for attributes of a dataclass created in post_init


Python: 3.7+

I have a dataclass and a subclass of it as following:

from abc import ABC
from dataclasses import dataclass
from typing import Dict, List, Optional

from dbconn import DBConnector


@dataclass
class User:
  uid: int
  name: str


@dataclass
class Model(ABC):
  database: DBConnector
  user: User

  def func(self, *args, **kwargs):
    pass


@dataclass
class Command(Model):
  message: Optional[str] = "Hello"

  def __post_init__(self):
    self.user_id: str = str(self.user.uid)
    self.message = f"{self.user.name}: {self.message}"

I could get the type hint for database, user and message using typing.get_type_hints(Command). How can I get the type hints for user_id?

One workaround would to be pass in the user.uid and user.name as separate params to Command but that's not pragmatic when User object has many useful attributes.

I believe the reason why it doesn't work in the first place is because init gets called at runtime and that's why type checking doesn't take those attrs into account. One possible solution would be to parse the ast of the class but I'm not sure if that's recommended and generic enough. If yes, would appreciate a working example.


Solution

  • Figured out a hacky solution by using inspect.get_source and regex matching Type Hinted attributes. Also had to convert dataclass into a normal class for the end Model.

    from abc import ABC
    from dataclasses import dataclass
    import inspect
    import re
    from typing import Dict, List, Optional
    
    from dbconn import DBConnector
    
    
    @dataclass
    class User:
        uid: int
        name: str
    
    
    @dataclass
    class Model(ABC):
        database: DBConnector
        user: User
    
        def func(self, *args, **kwargs):
            pass
    
        def get_type_hints(self):
            source = inspect.getsource(self.__class__)
            # Only need type hinted attributes
            patt = r"self\.(?P<name>.+):\s(?P<type>.+)\s="
            attrs = re.findall(patt, source)
            for attr in attrs:
                yield attr + (getattr(self, attr[0]), )
    
    
    class Command(Model):
        message: Optional[str] = "Hello"
    
        def __init__(
            self, database: DBConnector,
            user: User,
            message: Optional[str] = "Hello"
        ):
            super().__init__(database, user)
            self.user_id: str = str(self.user.uid)
            self.message: Optional[str] = f"{self.user.name}: {self.message}"
    
    
    cmd = Command(DBConnector(), User(123, 'Stack Overflow'))
    for attr in cmd.get_type_hints():
        print(attr)
    
    # Output
    ('user_id', 'str', '123')
    ('message', 'str', 'Stack Overflow: Hello')
    

    If someone can come up with a more robust solution, I'm definitely interested in it. For now, I'll mark this as my answer, in case someone stumbles upon this and is ok with a hacky solution.