How to set the value of dataclass field in __post_init__ when frozen=True?

Use the same thing the generated __init__ method does: object.__setattr__.

def __post_init__(self):
    object.__setattr__(self, 'value', RANKS.index(self.rank) + 1)

Using mutation

Frozen objects should not be changed. But once in a while the need may arise. The accepted answer works perfectly for that. Here is another way of approaching this: return a new instance with the changed values. This may be overkill for some cases, but it's an option.

from copy import deepcopy

@dataclass(frozen=True)
class A:
    a: str = ''
    b: int = 0

    def mutate(self, **options):
        new_config = deepcopy(self.__dict__)
        # some validation here
        new_config.update(options)
        return self.__class__(**new_config)

Another approach

If you want to set all or many of the values, you can call __init__ again inside __post_init__. Though there are not many use cases.

The following example is not practical, only for demonstrating the possibility.

from dataclasses import dataclass, InitVar


@dataclass(frozen=True)
class A:
    a: str = ''
    b: int = 0
    config: InitVar[dict] = None

    def __post_init__(self, config: dict):
        if config:
            self.__init__(**config)

The following call

A(config={'a':'a', 'b':1})

will yield

A(a='a', b=1)

without throwing error. This is tested on python 3.7 and 3.9.

Of course, you can directly construct using A(a='hi', b=1), but there maybe other uses, e.g. loading configs from a json file.

Bonus: an even crazier usage

A(config={'a':'a', 'b':1, 'config':{'a':'b'}})

will yield

A(a='b', b=1)