0

How do I specify the type of a callback function which takes an optional parameter?

For example, I have the following function:

def getParam(param: str = 'default'):
  return param

I'd like to pass it on as a callback function and use it both as Callable[[str], str] and Callable[[], str]:

def call(callback: Callable[[<???>], str]):
  return callback() + callback('a')

What do I write for <???>? I've tried Python's Optional but that does not work, as it still enforces a parameter (even if with value None) to be present.


For reference, I'm basically searching for the equivalent of the following TypeScript:

function call(callback: (a?: string) => string) {
  return callback() + callback('a');
}
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
DarkTrick
  • 2,447
  • 1
  • 21
  • 39
  • Does this answer your question? [How can I specify the function type in my type hints?](https://stackoverflow.com/questions/37835179/how-can-i-specify-the-function-type-in-my-type-hints) – dm2 Dec 22 '22 at 14:59
  • 1
    You just want a callable with no arguments - the fact that `getParam` achieves that by having one parameter but with a default value is completely irrelevant to `call`: https://mypy-play.net/?mypy=latest&python=3.11&gist=15accb9eb5f0a2d7b0d2bf00c54cd1c1. (Same [in TS](https://tsplay.dev/W4XQAW), for that matter.) – jonrsharpe Dec 22 '22 at 15:01
  • @jonrsharpe My example was too abbreviated. I edited the code, so the `call` function would use both ways. – DarkTrick Dec 22 '22 at 15:11
  • I think you want an intersection type, something that's both a `Callable[[str], str]` _and_ `Callable[[], str]`, but Python typing only supports unions. – jonrsharpe Dec 22 '22 at 15:14
  • 1
    Have you considered simply *requiring* an argument, and letting the caller pass `'default'` explicitly instead? `def call(callback: Callable[[str], str]): return callback('default') + callback('a')`. The circumstances under which a callback is used don't usually require the flexibility you are looking for. – chepner Dec 22 '22 at 15:15
  • 1
    As an alternative, use `def getParam(param: str = None)`, and let the callback replace `None` with the true default *inside* the function. Then you can truthfully say the type of `getParam` is `Callable[[Optional[str]], str]`. – chepner Dec 22 '22 at 15:18
  • @chepner but MyPy won't actually let you call a `Callable[[Optional[str]], str]` without any arguments: https://mypy-play.net/?mypy=latest&python=3.11&gist=50d1b1d2bd809ebdac33cfc6d59c8b63 ([_"this is not the same concept as an optional argument"_](https://docs.python.org/3/library/typing.html#typing.Optional)). – jonrsharpe Dec 22 '22 at 15:23
  • The *caller* is perfectly capable of writing `callback(None)` instead of `callback()`. Optional arguments for more for the benefit of lazy human typists. – chepner Dec 22 '22 at 15:24
  • @chepner Valid workaround! I'm concerned with the noise `None` is producing. – DarkTrick Dec 22 '22 at 15:29
  • What noise is that? – chepner Dec 22 '22 at 15:33
  • @chepner `callback()` is less cognitive load than `callback(None)` – DarkTrick Dec 23 '22 at 15:49
  • On who? Nobody calls `callback` explicitly. – chepner Dec 23 '22 at 15:56
  • @chepner IMO this will be a pointless subjective discussion on *why I believe so* and *you believe otherwise*. Let's skip that. But apart from that, I think your suggestion would also make a nice answer as a workaround. – DarkTrick Dec 23 '22 at 23:29

1 Answers1

0

There is no syntax to indicate optional or keyword arguments https://docs.python.org/3/library/typing.html#typing.Callable

However you can use something like this

from typing import Protocol


class MyCallback(Protocol):
    def __call__(self, x: int =5): ...


def foo(callback: MyCallback):
    callback()  # <- IDE is fine with both of these lines
    callback(7)
user2357112
  • 260,549
  • 28
  • 431
  • 505
Ron Serruya
  • 3,988
  • 1
  • 16
  • 26
  • This is interesting, but won't recognise that `getParam` implements `MyCallback` without further information: https://mypy-play.net/?mypy=latest&python=3.11&gist=4c92d1bcf457e3a37f4aa20e82d41e4b – jonrsharpe Dec 22 '22 at 15:30
  • @jonrsharpe: Unlike with `Callable`, you need to make sure the argument names match. – user2357112 Dec 22 '22 at 15:36
  • @user2357112 oh interesting - this answer would benefit from an example that actually applies to the OP's case – jonrsharpe Dec 22 '22 at 15:37
  • (You can mark the argument as positional-only in the protocol to remove the name match requirement.) – user2357112 Dec 22 '22 at 15:37