diff --git a/schema/__init__.py b/schema/__init__.py index 10d6e8a..0ecc7e2 100644 --- a/schema/__init__.py +++ b/schema/__init__.py @@ -2,6 +2,7 @@ obtained from config-files, forms, external services or command-line parsing, converted from JSON/YAML (or something else) to Python data-types.""" +import copy import inspect import re from typing import ( @@ -554,7 +555,9 @@ def validate(self, data: Any, **kwargs: Dict[str, Any]) -> Any: new[default.key] = ( _invoke_with_optional_kwargs(default.default, **kwargs) if callable(default.default) - else default.default + # Copy non-callable defaults so a mutable default (e.g. [] or + # {}) is not shared across validate() calls. + else copy.deepcopy(default.default) ) return new diff --git a/test_schema.py b/test_schema.py index 4d78456..8277b91 100644 --- a/test_schema.py +++ b/test_schema.py @@ -419,6 +419,17 @@ def test_dict_optional_defaults(): Optional(And(str, Use(int)), default=7) +def test_dict_optional_mutable_default_not_shared(): + # A mutable default (e.g. [] or {}) must not be shared across validate() + # calls: mutating one result must not leak into later ones. See GH-352. + s = Schema({Optional("items", default=[]): list}) + a = s.validate({}) + a["items"].append(1) + b = s.validate({}) + assert b["items"] == [] + assert a["items"] is not b["items"] + + def test_dict_subtypes(): d = defaultdict(int, key=1) v = Schema({"key": 1}).validate(d)