Cannot inherit non-frozen dataclass from a frozen one

I encountered this issue today with msgspec.

The crux of the issue is that mypy does not support the following (working) pattern:

import msgspec as ms


class Base(ms.Struct, frozen=True):
    ...


class Child(Base): # error: Cannot inherit non-frozen dataclass from a frozen one  [misc]
    ...


assert Child.__struct_config__.frozen is True
<...>frozen.py:8: error: Cannot inherit non-frozen dataclass from a frozen one  [misc]
Found 1 error in 1 file (checked 1 source file)

From what I can gather, my options are to either add a # type: ignore[misc] everytime I inherit from my base class or directly inherit from Struct and dupe the config every time. But neither of those are satisfactory, so I’m opening this discussion in hopes of finding something better.

I get a different error from mypy when trying to reproduce this with some dummy fields

inherit_frozen_dataclass.py:10: error: Unexpected keyword argument "frozen" for "__init_subclass__" of "object"  [call-arg]
inherit_frozen_dataclass.py:18: error: "type[Child]" has no attribute "__struct_config__"  [attr-defined]

I think you need to re-apply the decorator on the subclasses: dataclasses — Data Classes — Python 3.13.1 documentation If even the decorator doesn’t work, perhaps a work around is possible with the factory: dataclasses — Data Classes — Python 3.13.1 documentation

With regards your question,
Is it possible to use composition instead of inheritance (can Child own its own Base, instead of being a Base)?

Otherwise can the extra fields to be added to Base go into a mix-in?

Or can you define a NonFrozenBaseClass, inherit from it, and make the frozen version, Base, afterwards?

1 Like

msgspec relies on dataclass_transform for typing, which makes assumptions that these behave like dataclasses and in that case frozen wouldn’t be inherited.

I think you should be able to use dataclass_transform with frozen_default=True like this to appease mypy.

import msgspec as ms
from typing import dataclass_transform


@dataclass_transform(field_specifiers=(ms.field,), frozen_default=True)
class Base(ms.Struct, frozen=True):
    ...


class Child(Base):
    ...


assert Child.__struct_config__.frozen is True
1 Like

@dataclass_transform(frozen_default=True) does appease mypy, thank you.

Not really, my use case is something like:

import msgspec

class Base(msgspec.Struct, frozen=True, eq=True, cache_hash=True, dict=True):
    pass

class Title(Base):
    english: str | None = None
    native: str | None = None


class Date(Base):
    year: int | None = None
    month: int | None = None
    day: int | None = None

# more classes

I think this also works if I’m doing it correctly:

import msgspec


class BaseMixin(msgspec.Struct, frozen=True, eq=False):
    pass


class Point(BaseMixin, msgspec.Struct):
    x: int
    y: int


print(Point(0, 1) == Point(0, 1)) # False

This works and what the author suggested in the original issue

import msgspec


class Base(msgspec.Struct, eq=False):
    pass


class Point(Base, frozen=True):
    x: int
    y: int


print(Point(0, 1) == Point(0, 1)) # False