Skip to content

Commit d3bb2d2

Browse files
committed
Add additional examples about TypeIs
1 parent 75caed7 commit d3bb2d2

File tree

1 file changed

+75
-0
lines changed

1 file changed

+75
-0
lines changed

docs/spec/narrowing.rst

+75
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,78 @@ This code fails at runtime, because the narrower returns ``False`` (1 is not a `
227227
and the ``else`` branch is taken in ``takes_narrower()``.
228228
If the call ``takes_narrower(1, is_bool)`` was allowed, type checkers would fail to
229229
detect this error.
230+
231+
In some cases, it may not be possible to narrow a type fully from information
232+
available to the TypeIs function. In such cases, raising an error is the only
233+
possible option, as you have neither enough information to confirm or deny a
234+
type narrowing operation. This is most likely to occur with narrowing of generics.
235+
236+
To see why, we can look at the following example::
237+
238+
from typing_extensions import TypeVar, TypeIs
239+
from typing import Generic
240+
241+
X = TypeVar("X", str, int, str | int, covariant=True, default=str | int)
242+
243+
class A(Generic[X]):
244+
def __init__(self, i: X, /):
245+
self._i: X = i
246+
247+
@property
248+
def i(self) -> X:
249+
return self._i
250+
251+
252+
class B(A[X], Generic[X]):
253+
def __init__(self, i: X, j: X, /):
254+
super().__init__(i)
255+
self._j: X = j
256+
257+
@property
258+
def j(self) -> X:
259+
return self._j
260+
261+
def possible_problem(x: A) -> TypeIs[A[int]]:
262+
return isinstance(x.i, int)
263+
264+
def possible_correction(x: A) -> TypeIs[A[int]]:
265+
if type(x) is A:
266+
# only narrow cases we know about
267+
return isinstance(x.i, int)
268+
raise TypeError(
269+
f"Refusing to narrow Genenric type {type(x)!r}"
270+
f"from function that only knows about {A!r}"
271+
)
272+
273+
Because it is possible to attempt to narrow B,
274+
but A does not have appropriate information about B
275+
(or any other unknown subclass of A!) it's not possible to safely narrow
276+
in either direction. The general rule for generics is that if you do not know
277+
all the places a generic class is generic and do not enough of them to be
278+
absolutely certain, you cannot return True, and if you do not have a definitive
279+
counter example to the type to be narrowed to you cannot return False.
280+
In practice, if soundness is prioritized over an unsafe narrowing,
281+
not knowing what you don't know is solvable by erroring out
282+
or by making the class to be narrowed final to avoid such a situation.
283+
284+
In practice, such correctness is not always neccessary, and may work against
285+
your needs. for example, if you trust that users implementing
286+
the Sequence Protocol are doing so in a way that is safe to iterate over,
287+
the following function can never be fully sound, but fully soundness is not necessarily
288+
easier or better for your use::
289+
290+
def useful_unsoundness(s: Sequence[object]) -> TypeIs[Sequence[int]]:
291+
return all(isinstance(i, int) for i in s)
292+
293+
However, many cases of this sort can be extracted for safe use with an alternative construction
294+
if soundness is of a high priority, and the cost of a copy is acceptable::
295+
296+
def safer(s: Sequence[object]) -> Sequence[int]:
297+
ret = tuple(i for i in s if isinstance(i, int))
298+
if len(ret) != len(s):
299+
raise TypeError
300+
return ret
301+
302+
Ultimately, TypeIs allows a very large amount of flexibility in handling type-narrowing,
303+
at the cost of more of the issues of evaluating when it is use is safe being left
304+
in the hands of developers.

0 commit comments

Comments
 (0)