Open Feuermurmel opened 7 months ago
In typed code I would prefer checking explicitly for None instead of all falsy values:
def run_until_not_none[T](fn: Callable[[], T | None], num_tries: int = 5) -> T:
for _ in range(num_tries):
if (res := fn()) is not None:
return res
Exception('Giving up.')
This can already be typed and is less likely to lead to surprises.
Would this work for your case?
Would this work for your case?
I don't think addresses by use case. The type returned by run_until_truthy(foo)
is still str | None
, even though it will never return None
: Playground
In typed code I would prefer checking explicitly for None instead of all falsy values:
def run_until_not_none[T](fn: Callable[[], T | None], num_tries: int = 5) -> T: for _ in range(num_tries): if (res := fn()) is not None: return res Exception('Giving up.')
This can already be typed and is less likely to lead to surprises.
If I wrote that part of the application now, I would definitely use this approach. But I have found existing usages that make use of the fact that the function will be re-run on return values []
and ""
.
Another case where these types could be useful is when typing functions that have behavior similar to the and
and or
operators: Playground
from typing import Literal, Protocol
class Truthy(Protocol):
def __bool__(self) -> Literal[True]: ...
class Falsy(Protocol):
def __bool__(self) -> Literal[False]: ...
This doesn't really appear to work in pyright or mypy however, since None
is special cased and doesn't actually follow the NoneType
annotation for __bool__
on typeshed, for the others it doesn't work, since Literal[0, "", b"", False]
aren't special cased to fulfil this Protocol
.
But I would seriously question the value of making something like this work, since excluding a single literal from a type is usually not that helpful (Literal[True, False]
are kind of the exception). I think excluding None
is usually sufficient and will give you the result you want in most cases.
Of course you could write a couple of simple TypeGuard
s to convert any value into Truthy
/Falsy
, but that wouldn't be particularly ergonomic to use and doesn't help with the given example (but it may help with the and
/or
usecase)
But I would seriously question the value of making something like this work, since excluding a single literal from a type is usually not that helpful (
Literal[True, False]
are kind of the exception). I think excludingNone
is usually sufficient and will give you the result you want in most cases.Of course you could write a couple of simple
TypeGuard
s to convert any value intoTruthy
/Falsy
, but that wouldn't be particularly ergonomic to use and doesn't help with the given example (but it may help with theand
/or
usecase)
My request stems purely from typing existing code. Partly, that code is a result of how Python employs the concept of truthy/falsy in boolean contexts. Because of typing ergonomics, I usually refrain from relying on that concept (e.g. bar if foo is None else None
rather than foo or bar
), but the language is still pushing me in the direction of using these concepts and I see a lot of code being written that relies on it and is hard to type.
I found this function in our codebase and I'm trying to type it:
The function repeatedly calls a callable without arguments until it returns a "truthy" value. Then it returns that value. Easy enough:
This is correct but too restrictive. Usages like this don't work:
I can hack it to make it work:
Does it make sense to add official
Truthy
andFalsy
types to the standard library?Maybe a
Truthy
type would also make sense.Truthy
can't be defined as a type alias of existing types, but could probably also be useful sometimes, e.g. to type the following function:From thinking about it for a few minutes, I think
Falsy
would be more often useful thanTruthy
, at least in combination with the current typing features.If there's an intersection type constructor at some point, the above examples could be written like this instead, which might be easier to understand: