{"id":"4ce926a86594c63a","repo":"pydantic/pydantic","slug":"frozen-instance","errorCode":"frozen_instance","errorMessage":"frozen_instance","messagePattern":"frozen_instance","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"pydantic/main.py","lineNumber":104,"sourceCode":"\nRebuilding a model isn't thread-safe (the class attributes are mutated during the rebuild,\nwhile other threads may be reading them to perform validation/serialization), so `model_rebuild()`\ncalls are serialized using this lock. The lock is reentrant, as rebuilding a model can trigger the\nrebuild of another one (e.g. when a generic origin is rebuilt during parametrization in\n`__class_getitem__()`). For the same reason, the lock is global and not per-class: two threads\nholding their own class's lock could otherwise request the other's and deadlock.\n\"\"\"\n\n\ndef _check_frozen(model_cls: type[BaseModel], name: str, value: Any) -> None:\n    if model_cls.model_config.get('frozen'):\n        error_type = 'frozen_instance'\n    elif getattr(model_cls.__pydantic_fields__.get(name), 'frozen', False):\n        error_type = 'frozen_field'\n    else:\n        return\n\n    raise ValidationError.from_exception_data(\n        model_cls.__name__, [{'type': error_type, 'loc': (name,), 'input': value}]\n    )\n\n\ndef _model_field_setattr_handler(model: BaseModel, name: str, val: Any) -> None:\n    model.__dict__[name] = val  # pyright: ignore[reportIndexIssue] (https://github.com/microsoft/pyright/issues/11548)\n    model.__pydantic_fields_set__.add(name)\n\n\ndef _private_setattr_handler(model: BaseModel, name: str, val: Any) -> None:\n    if getattr(model, '__pydantic_private__', None) is None:\n        # While the attribute should be present at this point, this may not be the case if\n        # users do unusual stuff with `model_post_init()` (which is where the  `__pydantic_private__`\n        # is initialized, by wrapping the user-defined `model_post_init()`), e.g. if they mock\n        # the `model_post_init()` call. Ideally we should find a better way to init private attrs.\n        object.__setattr__(model, '__pydantic_private__', {})\n    model.__pydantic_private__[name] = val  # pyright: ignore[reportOptionalSubscript]\n","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/pydantic/pydantic/blob/2e5f0e2b4218de31709f1cf9c5bc61ea97a68835/pydantic/main.py#L86-L122","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use model_copy(update={'field': value}) to return a new immutable instance instead of mutating.","If mutation is genuinely required, remove `frozen=True` from model_config (and reconsider why it was set).","For per-field immutability, set `Field(..., frozen=True)` only on specific fields instead of freezing the whole model."],"exampleFix":"# before\nfrom pydantic import BaseModel, ConfigDict\n\nclass M(BaseModel):\n    model_config = ConfigDict(frozen=True)\n    x: int\n\nm = M(x=1)\nm.x = 2  # raises frozen_instance ValidationError\n\n# after\nm2 = m.model_copy(update={'x': 2})","handlingStrategy":"validation","validationCode":"from pydantic import BaseModel\n\ndef safe_set(model: BaseModel, name: str, value):\n    if model.model_config.get('frozen'):\n        return model.model_copy(update={name: value})\n    setattr(model, name, value)\n    return model","typeGuard":"from pydantic import BaseModel\n\ndef is_frozen(model: BaseModel) -> bool:\n    return bool(model.model_config.get('frozen'))","tryCatchPattern":"from pydantic import ValidationError\n\ntry:\n    m.x = 2\nexcept ValidationError as e:\n    if any(err['type'] == 'frozen_instance' for err in e.errors()):\n        m = m.model_copy(update={'x': 2})\n    else:\n        raise","preventionTips":["Treat frozen models as immutable values; always use model_copy(update=...).","Only set frozen=True when you need hashability, and audit call sites that mutate.","For per-field immutability prefer Field(frozen=True) over class-wide frozen."],"tags":["pydantic","frozen","immutability","validation-error","set-attribute"],"analyzedSha":"2e5f0e2b4218de31709f1cf9c5bc61ea97a68835","analyzedAt":"2026-08-04T19:54:21.281Z","schemaVersion":2}