diff --git a/schema/__init__.py b/schema/__init__.py index 10d6e8a..c59d704 100644 --- a/schema/__init__.py +++ b/schema/__init__.py @@ -905,7 +905,15 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: f'"{self._schema!r}" is too complex.' ) self.default = default - self.key = str(self._schema) + # Preserve the original key object so the default fills the output + # under the same key (and key type) that a present key would use. + # Stringifying here made an absent optional produce ``str(key)`` + # (e.g. ``"5"``) while a present key kept its real type (``5``). + self.key = ( + self._schema.schema + if isinstance(self._schema, Literal) + else self._schema + ) def __hash__(self) -> int: return hash(self._schema) diff --git a/test_schema.py b/test_schema.py index 4d78456..9333c68 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_default_preserves_key_type(): + # A non-string key must keep its type when the default fills it in, just + # as it does when the key is present in the data. + for key in (5, None, 1.5): + schema = Schema({Optional(key, default="x"): str}) + assert schema.validate({}) == {key: "x"} + # present and absent must agree on the output key type + present = {key: "p"} + assert schema.validate(present) == present + + def test_dict_subtypes(): d = defaultdict(int, key=1) v = Schema({"key": 1}).validate(d)