pydantic/pydantic · error · ValidationError

frozen_instance

frozen_instance

Error message

frozen_instance

What it means

Raised by _check_frozen (main.py:96-106) when model_config has `frozen=True` (class-wide) and any attribute assignment is attempted. It surfaces as a ValidationError with error type 'frozen_instance' and the offending field name/value in loc/input. This is the same machinery dataclasses use to make instances hashable/immutable.

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 2e5f0e2b42)

Solutions

  1. Use model_copy(update={'field': value}) to return a new immutable instance instead of mutating.
  2. If mutation is genuinely required, remove `frozen=True` from model_config (and reconsider why it was set).
  3. For per-field immutability, set `Field(..., frozen=True)` only on specific fields instead of freezing the whole model.

Example fix

# before
from pydantic import BaseModel, ConfigDict

class M(BaseModel):
    model_config = ConfigDict(frozen=True)
    x: int

m = M(x=1)
m.x = 2  # raises frozen_instance ValidationError

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

Strategy: validation

Validate before calling

from pydantic import BaseModel

def safe_set(model: BaseModel, name: str, value):
    if model.model_config.get('frozen'):
        return model.model_copy(update={name: value})
    setattr(model, name, value)
    return model

Type guard

from pydantic import BaseModel

def is_frozen(model: BaseModel) -> bool:
    return bool(model.model_config.get('frozen'))

Try / catch

from pydantic import ValidationError

try:
    m.x = 2
except ValidationError as e:
    if any(err['type'] == 'frozen_instance' for err in e.errors()):
        m = m.model_copy(update={'x': 2})
    else:
        raise

Prevention

When it happens

Trigger: Defining `model_config = ConfigDict(frozen=True)` then doing `instance.field = value`, or calling setattr()/copy-with-update on a frozen model. Triggered inside __setattr__ via _check_frozen before any field write.

Common situations: Making models hashable so they can go in sets/dicts (requires frozen=True); ORM-style mutation code that expects to update fields in place; migrating from mutable v1 models to frozen v2 models.

Related errors


AI-assisted analysis of pydantic/pydantic@2e5f0e2b42 (2026-08-04). Data as JSON: /data/errors/4ce926a86594c63a.json. Report an issue: GitHub.