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