0

In a recent project of mine I encountered a genuine reason to have a parameter, which accepts a function, with a default value.

It looks like this, ignoring the context.

def foo(func: Callable[[], None]) -> None:
    ...

My initial thought was to set the default to lambda: None. Indicating that the default value is a function that accepts no parameters and returns None. i.e.

def foo(func: Callable[[], None] = lambda: None) -> None:
        ...

After some thought I figured lambda: ... would also get that point across, because I found lambda: None to look a bit strange. i.e.

def foo(func: Callable[[], None] = lambda: ...) -> None:
        ...

My question to you is, which of these is better? I am also open to suggestions outside of these two.

Edit: using Callable to type hint the parameter. The second example in fact returns a non-None value, which does not actually matter since in context the return value is not used--however, that is a noteworthy discrepancy.

Cresht
  • 1,020
  • 2
  • 6
  • 15
  • I thought I understood your question and posted an answer - but are you saying you already have `function` defined and just need the most Pythonic way to provide a default function that just passes? – Grismar Nov 13 '21 at 04:04
  • `...` is the `Ellipsis` constant. It's intended for use in third-party libraries that use multidimensional indexing (e.g. `numpy`). Using `...` loosely implies it returns *something* (which it does), where `None` is returning "nothing" (logical nothing anyway; even `None` is an object in Python, it's not quite the same as a `NULL` pointer in a lower level language). – ShadowRanger Nov 13 '21 at 04:06
  • @ShadowRanger I don't think that's correct, the Ellispses is often used as a style choice over `pass`, both have the function return `None` – Grismar Nov 13 '21 at 04:07
  • @Cresht I don't know what version of Python, or what packages, you're using but `function` is most definitely not a class in standard Python. – Grismar Nov 13 '21 at 04:08
  • @Grismar: Not in a `lambda`; `lambda`s return the result of the expression. In a `def`, the load of `Ellipsis` gets optimized out when it's not explicitly `return`ed or assigned, but you can't do that when the `lambda` is actually returning it. Speed-wise, there's no difference (they're both loading a global singleton constant), it's just that `Ellipsis` is not "nothing" (among other things, it's truthy, where `None` is falsy). Even using `...` over `pass` is a mild misuse (it's less explicit about "not doing anything here") though I know some folks count it as acceptable. – ShadowRanger Nov 13 '21 at 04:09
  • @ShadowRanger I see, I assume the best way to dig into that would be to either have a look with `dis`, or the documentation? There's no obvious way to demonstrate with code? – Grismar Nov 13 '21 at 04:14
  • @Cresht although that's the reported type of the lambda, you can't just use it like you did, unless you manage to import it from somewhere (I wouldn't know where), but `Callable` seems to be what you ought to be using there? – Grismar Nov 13 '21 at 04:16
  • 1
    @Grismar: I'm not sure what you're asking. It's pretty easy to do `(lambda: ...)() is Ellipsis` to demonstrate that the `...` is actually giving the `lambda` a different return value. Or to do `print("Truthy" if ... else "Falsy")` to see these behaviors. – ShadowRanger Nov 13 '21 at 04:25
  • @ShadowRanger thanks, I was mainly after the first example - makes sense, but I appreciate you saving me the trouble of thinking about it ;-) – Grismar Nov 13 '21 at 09:08

1 Answers1

0

How about:

from typing import Callable


def foo(func: Callable) -> None:
    ...

See documentation here https://docs.python.org/3/library/typing.html#typing.Callable

Note that there's an important difference between these two examples you provided:

def foo(func: function) -> None:
    ...


def foo(func: function = lambda: ...) -> None:
    ...

In the first case (assuming function is defined, something like Callable, there is no default value and passing a value would be required.

In the second case, the type is the same, but there is a default value and the caller doesn't have to provide one. They make for a very different function.

At any rate, you're really just asking an opinion on style - since you're already using the Ellipsis as an alternative to pass, I feel this is the preferable option:

def foo(func: Callable = lambda: ...) -> None:
    ...

Although this is rather pointless, since your function in this form doesn't even call func, so providing a default parameter value does very little, as the type has already been explicitly specified.

Grismar
  • 27,561
  • 4
  • 31
  • 54
  • The first code block is just introducing the function that needs a default value for its parameter. I can remove it for clarity if you think that is better – Cresht Nov 13 '21 at 04:08