Jump to solution
Verify

The Fix

pip install pydantic==2.11.0

Based on closed pydantic/pydantic issue #11428 · PR/commit linked

Jump to Verify Open PR/Commit
@@ -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
repro.py
# 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
verify
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
fix.md
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).
Production impact:
  • 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”
@NikolayXHD · 2025-03-18 · source
“While the behavior can be confusing during the copy, the issue exists by itself when using cached properties”
@Viicos · 2025-02-11 · source
“> > * model_copy also copies the cached value of @cached_property”
@NikolayXHD · 2025-03-14 · source
“> * model_copy also copies the cached value of @cached_property”
@Viicos · 2025-02-12 · source

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
signature.txt
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.txt
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

repro.py
# 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

When NOT to use: This fix should not be used if cached properties are not intended to be mutable.

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.

When NOT to use: This fix should not be used if cached properties are not intended to be mutable.

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.

Get updates

We publish verified fixes weekly. No spam.

Subscribe

When NOT to Use This Fix

  • This fix should not be used if cached properties are not intended to be mutable.

Verify Fix

verify
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

VersionStatus
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.