diff --git a/changes/1491-PrettyWood.md b/changes/1491-PrettyWood.md new file mode 100644 index 00000000000..7d74dfef812 --- /dev/null +++ b/changes/1491-PrettyWood.md @@ -0,0 +1 @@ +Avoid side effects with `default_factory` by not calling it multiple times diff --git a/pydantic/fields.py b/pydantic/fields.py index c49ad7f0470..d4521624899 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -271,9 +271,9 @@ def __init__( self.model_config.prepare_field(self) self.prepare() - def get_default(self) -> Any: + def get_default(self, *, copy_factory: bool = False) -> Any: if self.default_factory is not None: - value = self.default_factory() + value = self.default_factory() if not copy_factory else deepcopy(self.default_factory)() elif self.default is None: # deepcopy is quite slow on None value = None @@ -296,7 +296,7 @@ def infer( if isinstance(value, FieldInfo): field_info = value - value = field_info.default_factory() if field_info.default_factory is not None else field_info.default + value = None if field_info.default_factory is not None else field_info.default else: field_info = FieldInfo(value, **field_info_from_config) required: 'BoolUndefined' = Undefined @@ -341,7 +341,7 @@ def prepare(self) -> None: Note: this method is **not** idempotent (because _type_analysis is not idempotent), e.g. calling it it multiple times may modify the field and configure it incorrectly. """ - default_value = self.get_default() + default_value = self.get_default(copy_factory=True) if default_value is not None and self.type_ is None: self.type_ = default_value.__class__ self.outer_type_ = self.type_ diff --git a/tests/test_main.py b/tests/test_main.py index fdbc9ea3f19..735c872edd3 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1088,6 +1088,27 @@ class FunctionModel(BaseModel): assert m.uid is uuid4 +def test_default_factory_side_effect(): + """It should call only once the given factory""" + + class Seq: + def __init__(self): + self.v = 0 + + def __call__(self): + self.v += 1 + return self.v + + class MyModel(BaseModel): + id: int = Field(default_factory=Seq()) + + m1 = MyModel() + assert m1.id == 1 + m2 = MyModel() + assert m2.id == 2 + assert m1.id == 1 + + @pytest.mark.skipif(sys.version_info < (3, 7), reason='field constraints are set but not enforced with python 3.6') def test_none_min_max_items(): # None default