The Fix
pip install pydantic==2.11.0
Based on closed pydantic/pydantic issue #11428 · PR/commit linked
@@ -29,7 +29,7 @@
import pydantic_core
import typing_extensions
-from pydantic_core import PydanticUndefined
+from pydantic_core import PydanticUndefined, ValidationError
from typing_extensions import Self, TypeAlias, Unpack
# test_pydantic_cache_prop.py
from functools import cached_property
import pytest
from pydantic import BaseModel, ConfigDict
from infrastructure.pydantic import patch_model_copy
patch_model_copy()
class MyClass(BaseModel):
int_field: int
@cached_property
def negative(self) -> int:
return -self.int_field
class MyFrozenClass(BaseModel):
model_config = ConfigDict(frozen=True)
int_field: int
@cached_property
def negative(self) -> int:
return -self.int_field
@pytest.mark.parametrize('cls', [MyClass, MyFrozenClass])
def test_when_cached_field_computed_copied_instance_recomputes_field(
cls: type[MyClass] | type[MyFrozenClass],
) -> None:
instance_1 = cls(int_field=1)
assert instance_1.int_field == 1
assert 'negative' not in instance_1.__dict__
assert instance_1.negative == -1 # trigger computing and caching
assert 'negative' in instance_1.__dict__
instance_2 = instance_1.model_copy(update={'int_field': 2})
assert instance_2.int_field == 2
assert instance_2.negative == -2
@pytest.mark.parametrize('cls', [MyClass, MyFrozenClass])
def test_when_cached_field_uninitialized_copy_does_not_raise(
cls: type[MyClass] | type[MyFrozenClass],
) -> None:
instance_1 = cls(int_field=1)
assert instance_1.int_field == 1
assert 'negative' not in instance_1.__dict__
instance_2 = instance_1.model_copy(update={'int_field': 2})
# no exception
assert instance_2.int_field == 2
assert instance_2.negative == -2
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
Option A — Upgrade to fixed release\npip install pydantic==2.11.0\nWhen NOT to use: This fix should not be used if cached properties are not intended to be mutable.\n\nOption C — Workaround\n- Manually invalidating the `@cached_property` works, unless the instance is frozen.\nWhen NOT to use: This fix should not be used if cached properties are not intended to be mutable.\n\n
Why This Fix Works in Production
- Trigger: - `model_copy(update={"foo": new_val})` is used. This makes a copy in which the field which the cached property is dependent on, is changed.
- Mechanism: Cached properties were incorrectly copied during model updates without invalidation
- Why the fix works: Allows cached properties to be altered on frozen models and clarifies the documentation of `model_copy()`. (first fixed release: 2.11.0).
- If left unfixed, this can cause silent data inconsistencies that propagate (bad cache entries, incorrect downstream decisions).
Why This Breaks in Prod
- Triggered by an upgrade/regression window: 2.11 breaks; 2.11.0 is the first fixed release.
- Shows up under Python 3.13 in real deployments (not just unit tests).
- Cached properties were incorrectly copied during model updates without invalidation
- Production symptom (often without a traceback): - `model_copy(update={"foo": new_val})` is used. This makes a copy in which the field which the cached property is dependent on, is changed.
Proof / Evidence
- GitHub issue: #11428
- Fix PR: https://github.com/pydantic/pydantic/pull/11432
- First fixed release: 2.11.0
- Affected versions: 2.11
- Reproduced locally: No (not executed)
- Last verified: 2026-02-09
- Confidence: 0.95
- Did this fix it?: Yes (upstream fix exists)
- Own content ratio: 0.46
Discussion
High-signal excerpts from the issue thread (symptoms, repros, edge-cases).
“Here's the workaround I've been using so far, hope it helps people who like myself are missing dataclasses.replace -like behavior of @cached_property”
“While the behavior can be confusing during the copy, the issue exists by itself when using cached properties”
“> > * model_copy also copies the cached value of @cached_property”
“> * model_copy also copies the cached value of @cached_property”
Failure Signature (Search String)
- - `model_copy(update={"foo": new_val})` is used. This makes a copy in which the field which the cached property is dependent on, is changed.
- del f_demo_foobar.bar # raises pydantic_core.ValidationError
Copy-friendly signature
Failure Signature
-----------------
- `model_copy(update={"foo": new_val})` is used. This makes a copy in which the field which the cached property is dependent on, is changed.
del f_demo_foobar.bar # raises pydantic_core.ValidationError
Error Message
Signature-only (no traceback captured)
Error Message
-------------
- `model_copy(update={"foo": new_val})` is used. This makes a copy in which the field which the cached property is dependent on, is changed.
del f_demo_foobar.bar # raises pydantic_core.ValidationError
Minimal Reproduction
# test_pydantic_cache_prop.py
from functools import cached_property
import pytest
from pydantic import BaseModel, ConfigDict
from infrastructure.pydantic import patch_model_copy
patch_model_copy()
class MyClass(BaseModel):
int_field: int
@cached_property
def negative(self) -> int:
return -self.int_field
class MyFrozenClass(BaseModel):
model_config = ConfigDict(frozen=True)
int_field: int
@cached_property
def negative(self) -> int:
return -self.int_field
@pytest.mark.parametrize('cls', [MyClass, MyFrozenClass])
def test_when_cached_field_computed_copied_instance_recomputes_field(
cls: type[MyClass] | type[MyFrozenClass],
) -> None:
instance_1 = cls(int_field=1)
assert instance_1.int_field == 1
assert 'negative' not in instance_1.__dict__
assert instance_1.negative == -1 # trigger computing and caching
assert 'negative' in instance_1.__dict__
instance_2 = instance_1.model_copy(update={'int_field': 2})
assert instance_2.int_field == 2
assert instance_2.negative == -2
@pytest.mark.parametrize('cls', [MyClass, MyFrozenClass])
def test_when_cached_field_uninitialized_copy_does_not_raise(
cls: type[MyClass] | type[MyFrozenClass],
) -> None:
instance_1 = cls(int_field=1)
assert instance_1.int_field == 1
assert 'negative' not in instance_1.__dict__
instance_2 = instance_1.model_copy(update={'int_field': 2})
# no exception
assert instance_2.int_field == 2
assert instance_2.negative == -2
Environment
- Python: 3.13
- Pydantic: 2
What Broke
Cached properties returned stale values after model updates, leading to inconsistent state.
Why It Broke
Cached properties were incorrectly copied during model updates without invalidation
Fix Options (Details)
Option A — Upgrade to fixed release Safe default (recommended)
pip install pydantic==2.11.0
Use when you can deploy the upstream fix. It is usually lower-risk than long-lived workarounds.
Option C — Workaround Temporary workaround
- Manually invalidating the `@cached_property` works, unless the instance is frozen.
Use only if you cannot change versions today. Treat this as a stopgap and remove once upgraded.
Fix reference: https://github.com/pydantic/pydantic/pull/11432
First fixed release: 2.11.0
Last verified: 2026-02-09. Validate in your environment.
When NOT to Use This Fix
- This fix should not be used if cached properties are not intended to be mutable.
Verify Fix
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
Did This Fix Work in Your Case?
Quick signal helps us prioritize which fixes to verify and improve.
Prevention
- Add a CI check that diffs key outputs after upgrades (OpenAPI schema snapshots, JSON payload shapes, CLI output).
- Upgrade behind a canary and run integration tests against the canary before 100% rollout.
Version Compatibility Table
| Version | Status |
|---|---|
| 2.11 | Broken |
| 2.11.0 | Fixed |
Related Issues
No related fixes found.
Sources
We don’t republish the full GitHub discussion text. Use the links above for context.