pydantic/pydantic · error · ValidationError

frozen_field

frozen_field

Error message

Field is frozen

What it means

An individual field declared `Field(frozen=True)` cannot be reassigned on an instance; the attempt raises a ValidationError with type `frozen_field` at that field's location. Unlike a whole-model `frozen` config, only that specific field is locked — other fields stay mutable.

Source

Thrown at pydantic/main.py:104

Rebuilding a model isn't thread-safe (the class attributes are mutated during the rebuild,
while other threads may be reading them to perform validation/serialization), so `model_rebuild()`
calls are serialized using this lock. The lock is reentrant, as rebuilding a model can trigger the
rebuild of another one (e.g. when a generic origin is rebuilt during parametrization in
`__class_getitem__()`). For the same reason, the lock is global and not per-class: two threads
holding their own class's lock could otherwise request the other's and deadlock.
"""


def _check_frozen(model_cls: type[BaseModel], name: str, value: Any) -> None:
    if model_cls.model_config.get('frozen'):
        error_type = 'frozen_instance'
    elif getattr(model_cls.__pydantic_fields__.get(name), 'frozen', False):
        error_type = 'frozen_field'
    else:
        return

    raise ValidationError.from_exception_data(
        model_cls.__name__, [{'type': error_type, 'loc': (name,), 'input': value}]
    )


def _model_field_setattr_handler(model: BaseModel, name: str, val: Any) -> None:
    model.__dict__[name] = val  # pyright: ignore[reportIndexIssue] (https://github.com/microsoft/pyright/issues/11548)
    model.__pydantic_fields_set__.add(name)


def _private_setattr_handler(model: BaseModel, name: str, val: Any) -> None:
    if getattr(model, '__pydantic_private__', None) is None:
        # While the attribute should be present at this point, this may not be the case if
        # users do unusual stuff with `model_post_init()` (which is where the  `__pydantic_private__`
        # is initialized, by wrapping the user-defined `model_post_init()`), e.g. if they mock
        # the `model_post_init()` call. Ideally we should find a better way to init private attrs.
        object.__setattr__(model, '__pydantic_private__', {})
    model.__pydantic_private__[name] = val  # pyright: ignore[reportOptionalSubscript]

View on GitHub (pinned to cc13d1b8c9)

Solutions

  1. Build a new instance with `instance.model_copy(update={...})` (note: model_copy bypasses validation, so ensure values are valid).
  2. Drop `frozen=True` from the field if it must be mutable.
  3. Reconstruct the model from `model_dump()` with the changed value.
  4. Reserve `frozen=True` for fields whose value should never change after init.

Example fix

# before
class M(BaseModel):
    id: int = Field(frozen=True)
    name: str
m = M(id=1, name='a')
m.id = 2  # raises frozen_field

# after
m = m.model_copy(update={'id': 2})
Defensive patterns

Strategy: validation

Validate before calling

from pydantic import BaseModel

def frozen_field_names(model_cls) -> set[str]:
    return {name for name, f in model_cls.model_fields.items() if getattr(f, 'frozen', False)}

# before mutating:
frozen = frozen_field_names(type(instance))
if name in frozen:
    instance = instance.model_copy(update={name: value})

Type guard

def field_is_frozen(model_cls, name: str) -> bool:
    return getattr(model_cls.model_fields.get(name), 'frozen', False)

Try / catch

from pydantic import ValidationError

try:
    setattr(instance, name, value)
except ValidationError as e:
    if any(err['type'] == 'frozen_field' for err in e.errors()):
        instance = instance.model_copy(update={name: value})
    else:
        raise

Prevention

When it happens

Trigger: `id: int = Field(frozen=True)` then `instance.id = 99` after construction.

Common situations: Selective immutability for primary keys, hashes, or compute-once fields; protecting identity fields from accidental overwrite.

Related errors


AI-assisted analysis of pydantic/pydantic@cc13d1b8c9 (2026-08-11). Data as JSON: /api/errors/db3856415303f2a8. Report an issue: GitHub.