From 584f29c38c8fc9b56ac1b7fc9bc8ed28e9c69e99 Mon Sep 17 00:00:00 2001 From: Labib-Bin-Salam Date: Fri, 19 Jun 2026 21:43:12 +0100 Subject: [PATCH] fix: allow C-implemented built-ins as Optional defaults `_invoke_with_optional_kwargs` called `inspect.signature(f)` on the default callable, but that raises `ValueError` for many C-implemented built-ins (e.g. `dict`, `int`). This made them unusable as `Optional` defaults, so Schema({Optional("k", default=dict): dict}).validate({}) raised instead of returning `{"k": {}}`. Catch the introspection failure and fall back to calling the default with no arguments -- the only sensible call for such callables, and the behaviour they had before validate() keyword arguments were forwarded to defaults. Callables that do expose a signature are unaffected, so keyword-argument forwarding still works. Closes #272. --- schema/__init__.py | 10 +++++++++- test_schema.py | 13 +++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/schema/__init__.py b/schema/__init__.py index 287d958..bf70a58 100644 --- a/schema/__init__.py +++ b/schema/__init__.py @@ -350,7 +350,15 @@ def _priority(s: Any) -> int: def _invoke_with_optional_kwargs(f: Callable[..., Any], **kwargs: Any) -> Any: - s = inspect.signature(f) + try: + s = inspect.signature(f) + except (ValueError, TypeError): + # Some callables -- notably C-implemented built-ins such as ``dict`` + # or ``int`` -- do not expose an introspectable signature. We cannot + # tell whether they accept keyword arguments, so fall back to calling + # them without any, matching how such defaults behaved before + # validate() keyword arguments were forwarded to them. + return f() if len(s.parameters) == 0: return f() return f(**kwargs) diff --git a/test_schema.py b/test_schema.py index e45d51f..476c961 100644 --- a/test_schema.py +++ b/test_schema.py @@ -838,6 +838,19 @@ def convert(data, increment): assert d["k"] == 1 and d["d"]["k"] == 42 and d["d"]["l"][0]["l"] == [3, 4, 5] +def test_optional_callable_default_builtin_c_callables(): + # ``inspect.signature`` raises for some C-implemented built-ins (e.g. + # ``dict`` and ``int``), which previously made them unusable as + # ``Optional`` defaults. See https://github.com/keleshev/schema/issues/272 + assert Schema({Optional("k", default=dict): dict}).validate({}) == {"k": {}} + assert Schema({Optional("k", default=int): int}).validate({}) == {"k": 0} + assert Schema({Optional("k", default=list): list}).validate({}) == {"k": []} + # validate() keyword arguments are still forwarded to callables that + # expose an introspectable signature accepting them. + s = Schema({Optional("k", default=lambda **kw: kw["increment"]): int}) + assert s.validate({}, increment=5) == {"k": 5} + + def test_inheritance_optional(): def convert(data, increment): if isinstance(data, int):