2

Which type hint should I give to an attribute which changes type in the __post_init__ method?

In the below example the argument passed to the class instance is of type int. However, it gets converted to type str. What is the correct type hint to show?

from dataclasses import dataclass

@dataclass
class Base:

   apples: int   # Should the type hint be 'int' OR 'int | str'
   
   def __post_init__(self):
      self.apples = str(self.apples)   # Type changes from int to str


b = Base(apples=1)   # Type is initially int

I am reusing the same attribute name here because I would like it show up in the __repr__ of the dataclass.

Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
kav
  • 507
  • 1
  • 8
  • 14
  • 2
    Why does the type change? Your type hints should include every valid/acceptable type – Iain Shelvington Aug 28 '21 at 03:55
  • In my case I have an input which is in format tuple and later gets changed to dt.date. I was curious if the type hint just needs to be valid for the initial parameter type or if it should have information about the future type as well. – kav Aug 28 '21 at 04:06
  • You should use the "future" type so that hinting tools know the actual type of the field. If you need to accept and convert other types when initialising then maybe this should be done in `__init__` or add a custom classmethod/constructor that accepts the other type and converts it – Iain Shelvington Aug 28 '21 at 04:12
  • 1
    The *attribute* has type `str`. The argument to init should be type `int`. Maybe just *don't use a data class then*. Or implement `__init_` manually – juanpa.arrivillaga Aug 28 '21 at 04:25
  • Does this answer your question? [Python dataclasses: What type to use if \_\_post\_init\_\_ performs type conversion?](https://stackoverflow.com/questions/51737828/python-dataclasses-what-type-to-use-if-post-init-performs-type-conversion) – Alex Waygood Aug 28 '21 at 07:28

2 Answers2

2

You can take the union of two types:

from typing import Union

@dataclass
class Base:
   apples: Union[int, str]
   
   def __post_init__(self):
      self.apples = str(self.apples)   # Type changes from int to str

But in most cases, this is not a good pattern since it makes typing less useful, and you can do things in some other way.

For example, you might do something like define a string property which is derived from some underlying data:

@dataclass
class Base:
    created_at: datetime
   
    @property
    def created_at_str(self) -> str:
        return self.created_at.strftime("%Y-%m-%d %H:%M:%S")

Edit: Based on your comment, if you want the __init__ method to take a tuple and then convert it to a date attribute in the __post_init__, this might be a case where you want to just define your own __init__ method and do it there rather than relying on the constructor that is autogenerated by the dataclass decorator.

Andrew Eckart
  • 1,618
  • 9
  • 15
-1

Here's a solution using field properties. An IDE understands that field apples passed in to __init__ can be of both types, and also understands that attribute b.apples will only be of constrained type str.

from dataclasses import dataclass, field
from typing import Union

from dataclass_wizard import property_wizard


@dataclass
class Base(metaclass=property_wizard):
    apples: Union[int, str]     # This annotation is for valid types passed in to constructor.
    # Not needed; added for better IDE support.
    _apples: str = field(init=False)

    # Annotate the expected return value of attribute here
    @property
    def apples(self) -> str:
        return self._apples

    @apples.setter
    def apples(self, apples: Union[int, str]):
        self._apples = str(apples)


b = Base(apples=1)  # Type is initially int

b
# Base(apples='1')

b.apples = 2
b.apples
# 2

type(b.apples)
# <class 'str'>

Disclaimer: I am the creator (and maintainer) of this library.

Dharman
  • 30,962
  • 25
  • 85
  • 135
rv.kvetch
  • 9,940
  • 3
  • 24
  • 53
  • This answer would be more useful if you explained why property wizard was helpful. One way would be to show how one would do it without wizard first. – Ben Jones Jan 22 '23 at 15:56