Я в таких случаях создаю фабричный метод.
@dataclass
class Person:
first_name: str
last_name: str
bdate: date
@classmethod
def make(cls, first_name: str, last_name: str, bdate: str) -> 'Person':
_bdate = datetime.strptime(bdate, '%Y%m%d').date()
return cls(first_name=first_name, last_name=last_name, bdate=_bdate)
data = {
'first_name': 'Adam',
'last_name': 'Smith',
'bdate': '20220617'
}
person = Person.make(**data)
Просто, коротко, позволяет реализовать любую логику приведения типов и вычисления значений по умолчанию, использует базовые механизмы Питона, при необходимости можно проигнорировать и использовать обычный конструктор (который мы не ломаем).
Но вообще это неправильное распределение обязанностей. Обязанность датакласса - хранить данные, а не менять их представление. За смену представления пусть отвечает тот код, который получает значение строки.