Let’s say I have some function
def do_some_stuff(
*,
foo: int,
bar: float | None = None,
baz: BazEnum = BazEnum.SPAM,
) -> int:
...
And some wrapper around it, for example to add default/error handling:
def do_some_stuff_safe(
*,
default: int = 3,
**kwargs: Any,
) -> int:
try:
return do_some_stuff(**kwargs)
except ValueError:
return default
I’d like to express that **kwargs must match do_some_stuff signature using the static type system.
Current solutions
In Python 3.12, I see two ways to achieve this:
- Explicitly re-expose
do_some_stuffkwargs indo_some_stuff_safe:def do_some_stuff_safe( *, foo: int, bar: float | None = None, baz: BazEnum = BazEnum.SPAM, default: int = 3, ) -> int: ... - Use
Unpack+TypedDictcombinaison introduced by PEP 692:class DoSomeStuffKwargs(TypedDict): foo: int bar: NotRequierd[float | None] baz: NotRequierd[BazEnum] def do_some_stuff_safe( *, default: int = 3, **kwargs: Unpack[DoSomeStuffKwargs], ) -> int: ...
These solutions have the same issue: we have to duplicate do_some_stuff’s signature information (argument names + types + default value in solution 1), with no mechanism that ensures they don’t go out of sync if we add, rename, delete… an argument in do_some_stuff.
(some possible mechanism)
One could write something in the lines of
if TYPE_CHECKING:
kwargs = cast(DoSomeStuffKwargs,{})
do_some_stuff(**kwargs) # type error if DoSomeStuffKwargs is incompatible with do_some_stuff signature
but that feels pretty hacky, and it does not detect addition of non-required argument.
Idea
I’d like to have a way to state that those kwargs are the ones of do_some_stuff, something like
def do_some_stuff_safe(
*,
default: int = 3,
**kwargs: Unpack[Kwargs[do_some_stuff]],
) -> int:
...
This reminds somewhat of ParamSpec in it’s usage, so we could maybe write it like ParmSpec(do_some_stuff).kwargs— allowing to use it with *args too!
Problem / question
My main issue with this idea is that having a function object inside a type annotation feels wrong, at it is not a type nor a special typing object— I think that’s not something done anywhere?
However, the function’s signature is a static available info, so it looks legitimate to rely on it… but I see no way to denote it other than through the function object.
In that extend, I would feel more comfortable with something ParamSpec()-ish (with parentheses) rather than Kwargs[]-ish (whith square brackets).