# Type checking subclasses of dataclass

**URL:** <https://discuss.python.org/t/type-checking-subclasses-of-dataclass/67561>\
**Category:** Python Help\
**Tags:** typing\
**Created:** [October 11, 2024, 1:16pm UTC](https://discuss.python.org/t/type-checking-subclasses-of-dataclass/67561 "2024-10-11T13:16:54Z")\
**Posts on this page:** 11\
**Page:** 1

<div class="post-metadata">

**Author:** ![abessman](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/abessman/32/8367_2.png) [@abessman](https://discuss.python.org/u/abessman)\
**Post date:** [October 11, 2024, 1:16pm UTC](https://discuss.python.org/t/type-checking-subclasses-of-dataclass/67561/1 "2024-10-11T13:16:54Z")

</div>

I’m trying to extend dataclass. I want to create something that is a dataclass, but with some extra features like being able to calculate its own size and serializing itself.

```python
import struct
from dataclasses import astuple, dataclass
from typing import Annotated

def datastructclass(cls=None, **kwargs):
    """Make a struct-like dataclass.

    Raises
    ------
    TypeError
        If any `field` does not contain `struct` metadata.
    """
    dcls = dataclass(cls)
    fmt_strs = []

    for f in fields(dcls):
        try:
            f.type. __metadata__
        except AttributeError as exc:
            msg = f"Field '{f.name}' missing annotation metadata"
            raise TypeError(msg) from exc

        try:
            fmt_strs.append(
                next(
                    filter(lambda m: isinstance(m, struct.Struct), f.type. __metadata__ )
                ).format
            )
        except StopIteration as exc:
            msg = f"Field '{f.name}' has no Struct metadata"
            raise TypeError(msg) from exc

    serializer = struct.Struct("".join(fmt_strs))

    def pack(self) -> bytes:
        return bytes(self)

    @classmethod
    def unpack(cls, buffer):
        return cls(*serializer.unpack(buffer))

    def __bytes__ (self) -> bytes:
        return serializer.pack(*astuple(self))

    dcls.format = serializer.format
    dcls.size = serializer.size
    dcls.pack = pack
    dcls.unpack = unpack
    dcls. __bytes__ = __bytes__
    return dcls

```

This works fine.

```python
import struct
from typing import Annotated

from datastructclass import datastructclass

@datastructclass
class A:
    a: Annotated[int, struct.Struct("H")] = 0

aaa = A.unpack(b"12")
print(aaa.a)
print(aaa.pack())
# Output:
# 12849
# b'12'

```

But mypy doesn’t like it:

```python
error: "type[A]" has no attribute "unpack" [attr-defined]

```

`type[A]` does, in fact, have an attribute `unpack`, but how can I convince mypy of this?

---

<div class="post-metadata">

**Author:** ![bschubert](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/bschubert/32/26920_2.png) [@bschubert](https://discuss.python.org/u/bschubert)\
**Post date:** [October 11, 2024, 1:54pm UTC](https://discuss.python.org/t/type-checking-subclasses-of-dataclass/67561/2 "2024-10-11T13:54:06Z")

</div>

Unfortunately, this isn’t well supported by Python’s type system. What you’d need is an [intersection type](https://github.com/python/typing/issues/213) to communicate that the return value of `datastructclass` is compatible both with `A` and with some protocol that describes the added `unpack`/`pack` functionality. Intersection types currently can’t be expressed in type annotations (though some type checkers support them internally).

As far as convincing mypy to make this work, you may have luck with implementing a [mypy plugin](https://mypy.readthedocs.io/en/stable/extending_mypy.html#extending-mypy-using-plugins) for your package.

See also this related thread for more on adding attributes with a decorator and intersection types (edit: d’oh, I just noticed you were the OP of that thread as well 🙂):

> [@How to type hint a class decorator?](https://discuss.python.org/t/how-to-type-hint-a-class-decorator/63010/):
>
> What is the correct way to annotate a decorator which takes a class as input and returns the same class as output? For example: def classdecorator(cls): cls.new\_attrib = True return cls @classdecorator class MyClass: pass MyClass.new\_attrib # True What is the type of cls in classdecorator, and what is its return type?

---

<div class="post-metadata">

**Author:** ![MegaIng](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/megaing/32/16162_2.png) [@MegaIng](https://discuss.python.org/u/MegaIng)\
**Post date:** [October 11, 2024, 2:06pm UTC](https://discuss.python.org/t/type-checking-subclasses-of-dataclass/67561/3 "2024-10-11T14:06:05Z")

</div>

Another good solution is to not use a decorate and instead use the base-class style of dataclass transforms, using ` __init_subclass__ `. The only drawback is that you can’t use `slots=True` since the class is already fully created then, but OTOH adding methods is not an issue at all and typing tools will pick them up correctly.

---

<div class="post-metadata">

**Author:** ![JamesParrott](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/jamesparrott/32/10534_2.png) [@JamesParrott](https://discuss.python.org/u/JamesParrott)\
**Post date:** [October 11, 2024, 2:46pm UTC](https://discuss.python.org/t/type-checking-subclasses-of-dataclass/67561/4 "2024-10-11T14:46:41Z")

</div>

Have you tried using attrs for this instead of dataclasses?

---

<div class="post-metadata">

**Author:** ![abessman](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/abessman/32/8367_2.png) [@abessman](https://discuss.python.org/u/abessman)\
**Post date:** [October 11, 2024, 7:36pm UTC](https://discuss.python.org/t/type-checking-subclasses-of-dataclass/67561/5 "2024-10-11T19:36:32Z")

</div>

> [@bschubert](#):
>
> See also this related thread for more on adding attributes with a decorator and intersection types (edit: d’oh, I just noticed you were the OP of that thread as well 🙂)

Right, I’d forgotten about that. It seems I encounter this particular problem, or a variation of it, every now and then and bash my head against it for at while.

> [@MegaIng](#):
>
> Another good solution is to not use a decorate and instead use the base-class style of dataclass transforms, using ` __init_subclass__ `.

Oh, nice, I had actually never encountered ` __init_subclass__ ` before! I’ll have to look at it more closely, but that looks like it might do the trick.

> [@JamesParrott](#):
>
> Have you tried using attrs for this instead of dataclasses?

I haven’t. I’ve never actually used attrs, thought I have encountered them a few times. Thanks for the suggestion, I’ll have a look to see if that could be a solution to my problem.

---

<div class="post-metadata">

**Author:** ![abessman](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/abessman/32/8367_2.png) [@abessman](https://discuss.python.org/u/abessman)\
**Post date:** [October 14, 2024, 6:30am UTC](https://discuss.python.org/t/type-checking-subclasses-of-dataclass/67561/6 "2024-10-14T06:30:27Z")

</div>

Unless I’m misunderstanding your suggestion, ` __init_subclass__ ` turns out to not work, unfortunately. ` __init_subclass__ ` is called too early in the class construction machinery, before the `dataclass` decorator, which means that the subclass’ `fields` aren’t set yet. The `fields` contain the type metadata needed in the serializer.

```python
from dataclasses import dataclass, fields

@dataclass
class FieldsPrinter:
    def __init_subclass__ (cls):
        print(fields(cls))

@dataclass
class MyDataclass(FieldsPrinter):
    a: int

mdc = MyDataclass(0)
# Output:
# ()

```

I’ll look into `attrs` next.

---

<div class="post-metadata">

**Author:** ![MegaIng](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/megaing/32/16162_2.png) [@MegaIng](https://discuss.python.org/u/MegaIng)\
**Post date:** [October 14, 2024, 6:33am UTC](https://discuss.python.org/t/type-checking-subclasses-of-dataclass/67561/7 "2024-10-14T06:33:19Z")

</div>

Don’t use the `dataclass` decorator explicitly at all, call it from the ` __init_subclass__ ` method. And decorator your base class with `dataclass_transform` so that type checkers know about it.

---

<div class="post-metadata">

**Author:** ![abessman](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/abessman/32/8367_2.png) [@abessman](https://discuss.python.org/u/abessman)\
**Post date:** [October 14, 2024, 6:53am UTC](https://discuss.python.org/t/type-checking-subclasses-of-dataclass/67561/8 "2024-10-14T06:53:51Z")

</div>

That gets me closer, thanks!

> **Code**
>
> ```python
> import struct
> from dataclasses import astuple, dataclass, fields
> from typing import Annotated, ClassVar, dataclass_transform
> 
> @dataclass_transform()
> class StructLike:
> _serializer: ClassVar[struct.Struct]
> 
> def __init_subclass__ (cls):
> dataclass(cls)
> fmt_strs = []
>     
> for f in fields(cls):
> try:
> f.type. __metadata__
> except AttributeError as exc:
> msg = f"Field '{f.name}' missing annotation metadata"
> raise TypeError(msg) from exc
>     
> try:
> fmt_strs.append(
> next(
> filter(lambda m: isinstance(m, struct.Struct), f.type. __metadata__ )
> ).format
> )
> except StopIteration as exc:
> msg = f"Field '{f.name}' has no Struct metadata"
> raise TypeError(msg) from exc
>     
> cls._serializer = struct.Struct("".join(fmt_strs))
> 
> def pack(self) -> bytes:
> return bytes(self)
> 
> @classmethod
> def unpack(cls, buffer):
> return cls(*cls._serializer.unpack(buffer))
> 
> def __bytes__ (self) -> bytes:
> return self._serializer.pack(*astuple(self))
> 
> class A(StructLike):
> a: Annotated[int, struct.Struct("H")] = 0
> 
> aaa = A.unpack(b"12")
> print(aaa.a)
> print(aaa.pack())
> 
> ```

Now mypy says:

> error: No overload variant of “astuple” matches argument type “StructLike” [call-overload]  
> note: Possible overload variants:  
> note: def astuple(obj: DataclassInstance) → tuple[Any, …]  
> note: def [\_T] astuple(obj: DataclassInstance, \*, tuple\_factory: Callable[[list[Any]], \_T]) → \_T

I tried to

```python
from __future__ import annotations
from typing import TYPE_CHECKING, dataclass_transform

if TYPE_CHECKING:
    from _typeshed import DataclassInstance

@dataclass_transform
class StructLike:
    def __init_subclass__ (cls: type[DataclassInstance]):
        ...

```

but this (specifically `from __future__ import annotations`) makes it not work?

```python
TypeError: Field 'a' missing annotation metadata

```

---

<div class="post-metadata">

**Author:** ![abessman](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/abessman/32/8367_2.png) [@abessman](https://discuss.python.org/u/abessman)\
**Post date:** [October 14, 2024, 7:02am UTC](https://discuss.python.org/t/type-checking-subclasses-of-dataclass/67561/9 "2024-10-14T07:02:38Z")

</div>

Well, I managed to satisfy mypy with this:

```diff
    def __bytes__ (self) -> bytes:
+ assert is_dataclass(self)
+ assert isinstance(self, StructLike)
        return self._serializer.pack(*astuple(self))

```

Feels a little hacky. Would welcome alternatives.

---

<div class="post-metadata">

**Author:** ![MegaIng](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/megaing/32/16162_2.png) [@MegaIng](https://discuss.python.org/u/MegaIng)\
**Post date:** [October 14, 2024, 7:08am UTC](https://discuss.python.org/t/type-checking-subclasses-of-dataclass/67561/10 "2024-10-14T07:08:32Z")

</div>

You might be able to apply `@dataclass` to StructLike directly to shut mypy up.

---

<div class="post-metadata">

**Author:** ![abessman](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/abessman/32/8367_2.png) [@abessman](https://discuss.python.org/u/abessman)\
**Post date:** [October 14, 2024, 7:33am UTC](https://discuss.python.org/t/type-checking-subclasses-of-dataclass/67561/11 "2024-10-14T07:33:26Z")

</div>

Yep, that works. Thanks!

```python
@dataclass
@dataclass_transform()
class StructLike:
    ...
# mypy: Success: no issues found in 1 source file

```
