(I wrote this: https://stackoverflow.com/a/79341515/20891286)
Python 3.10+

Uses Wizard.Ritvik’s answer and modified it so that the API is closer to normal dataclass, and supports all the parameters of field()

The signature_from function is just a utility for static type checkers and can be omitted.

Note that PyCharm, as of time of writing, has quite a few static type checking bugs and thus you may need to turn off “Incorrect call arguments” or it will complain about unfilled parameters in frozen_field()

from dataclasses import dataclass, field, fields
from typing import Callable, Generic, TypeVar, ParamSpec, Any


T = TypeVar("T")
P = ParamSpec("P")


class FrozenField(Generic[T]):
    """A descriptor that makes an attribute immutable after it has been set."""

    __slots__ = ("private_name",)

    def __init__(self, name: str) -> None:
        self.private_name = "_" + name

    def __get__(self, instance: object | None, owner: type[object] | None = None) -> T:
        value = getattr(instance, self.private_name)
        return value

    def __set__(self, instance: object, value: T) -> None:
        if hasattr(instance, self.private_name):
            msg = f"Attribute `{self.private_name[1:]}` is immutable!"
            raise TypeError(msg) from None

        setattr(instance, self.private_name, value)


# https://stackoverflow.com/questions/74714300/paramspec-for-a-pre-defined-function-without-using-generic-callablep
def signature_from(_original: Callable[P, T]) -> Callable[[Callable[P, T]], Callable[P, T]]:
    """Copies the signature of a function to another function."""

    def _fnc(fnc: Callable[P, T]) -> Callable[P, T]:
        return fnc

    return _fnc


@signature_from(field)
def frozen_field(**kwargs: Any):
    """A field that is immutable after it has been set. See `dataclasses.field` for more information."""
    metadata = kwargs.pop("metadata", {}) | {"frozen": True}
    return field(**kwargs, metadata=metadata)


def freeze_fields(cls: type[T]) -> type[T]:
    """
    A decorator that makes fields of a dataclass immutable, if they have the `frozen` metadata set to True.

    This is done by replacing the fields with FrozenField descriptors.

    Args:
        cls: The class to make immutable, must be a dataclass.

    Raises:
        TypeError: If cls is not a dataclass
    """

    cls_fields = getattr(cls, "__dataclass_fields__", None)
    if cls_fields is None:
        raise TypeError(f"{cls} is not a dataclass")

    params = getattr(cls, "__dataclass_params__")
    # _DataclassParams(init=True,repr=True,eq=True,order=True,unsafe_hash=False,
    #                   frozen=True,match_args=True,kw_only=False,slots=False,
    #                   weakref_slot=False)
    if params.frozen:
        return cls

    for f in fields(cls):  # type: ignore
        if "frozen" in f.metadata:
            setattr(cls, f.name, FrozenField(f.name))
    return cls


@freeze_fields
@dataclass(order=True)
class DC:
    stuff: int = frozen_field()


def main():
    dc = DC(stuff=3)
    print(repr(dc))  # DC(stuff=3)
    dc.stuff = 4  # TypeError: Attribute `stuff` is immutable!


if __name__ == "__main__":
    main()