@@ -227,3 +227,78 @@ This code fails at runtime, because the narrower returns ``False`` (1 is not a `
227227and the ``else `` branch is taken in ``takes_narrower() ``.
228228If the call ``takes_narrower(1, is_bool) `` was allowed, type checkers would fail to
229229detect 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