11

Let's say we have a trivial function that calls open() but with a fixed argument:

def open_for_writing(*args, **kwargs):
    kwargs['mode'] = 'w'
    return open(*args, **kwargs)

If I now try to call open_for_writing(some_fake_arg = 123), no type checker (e.g. mypy) can tell that this is an incorrect invocation: it's missing the required file argument, and is adding another argument that isn't part of the open signature.

How can I tell the type checker that *args and **kwargs must be a subset of the open parameter spec? I realise Python 3.10 has the new ParamSpec type, but it doesn't seem to apply here because you can't get the ParamSpec of a concrete function like open.

Tomerikoo
  • 18,379
  • 16
  • 47
  • 61
Migwell
  • 18,631
  • 21
  • 91
  • 160
  • 1
    Does this answer your question? [How to define a type for a function (arguments and return type) with a predefined type?](https://stackoverflow.com/questions/65182608/how-to-define-a-type-for-a-function-arguments-and-return-type-with-a-predefine) – MisterMiyagi Feb 25 '22 at 07:30
  • Do you actually need a subset? Your example would accept the same arguments as `open`, albeit silently ignoring one admittedly. – MisterMiyagi Feb 25 '22 at 07:35
  • @MisterMiyagi kind of. The main takeaway I get from that thread is that I can assign to `target.__annotations__`, which I didn't realise. Using a decorator isn't the only way to do that. – Migwell Feb 27 '22 at 06:07
  • 1
    Copying the `__annotations__` is just for runtime inspection. The important part is the annotation of the decorator itself, which copies the *static* type information between function. – MisterMiyagi Feb 27 '22 at 06:13
  • 1
    "because you can't get the ParamSpec of a concrete function" - this is such a shame. Was quite disappointing to find out that the new parameter typehinting feature is nowhere near typescript's `ReturnType` and `Parameters` :/ – olejorgenb Jul 07 '22 at 23:02

1 Answers1

7

I think out of the box this is not possible. However, you could write a decorator that takes the function that contains the arguments you want to get checked for (open in your case) as an input and returns the decorated function, i.e. open_for_writing in your case. This of course only works with python 3.10 or using typing_extensions as it makes use of ParamSpec

from typing import TypeVar, ParamSpec, Callable, Optional

T = TypeVar('T')
P = ParamSpec('P')


def take_annotation_from(this: Callable[P, Optional[T]]) -> Callable[[Callable], Callable[P, Optional[T]]]:
    def decorator(real_function: Callable) -> Callable[P, Optional[T]]:
        def new_function(*args: P.args, **kwargs: P.kwargs) -> Optional[T]:
            return real_function(*args, **kwargs)

        return new_function
    return decorator

@take_annotation_from(open)
def open_for_writing(*args, **kwargs):
    kwargs['mode'] = 'w'
    return open(*args, **kwargs)


open_for_writing(some_fake_arg=123)
open_for_writing(file='')

As shown here, mypy complains now about getting an unknown argument.

Simon Hawe
  • 3,968
  • 6
  • 14
  • Since you just copy the entire signature, there is no need to separate its signature. Parameterizing over the entire callable should do the same and be easier. See the proposed duplicate how to do that. – MisterMiyagi Feb 25 '22 at 07:33
  • Why do you define another nested function "new_function" instead of returning directly "real_function" ? – Eric M. May 10 '22 at 11:55