@@ -195,15 +195,91 @@ This behavior can be seen in the following example::
195
195
else:
196
196
reveal_type(x) # Unrelated
197
197
198
+ There are also cases beyond just mutability. In some cases, it may not be
199
+ possible to narrow a type fully from information available to the ``TypeIs ``
200
+ function. In such cases, raising an error is the only possible option, as you
201
+ have neither enough information to confirm or deny a type narrowing operation.
202
+ (Note: returning false (denying) results in unsafe negative narrowing in this
203
+ case) This is most likely to occur with narrowing of generics.
204
+
205
+ To see why, we can look at the following example::
206
+
207
+ from typing_extensions import TypeVar, TypeIs
208
+ from typing import Generic
209
+
210
+ X = TypeVar("X", str, int, str | int, covariant=True, default=str | int)
211
+
212
+ class A(Generic[X]):
213
+ def __init__(self, i: X, /):
214
+ self._i: X = i
215
+
216
+ @property
217
+ def i(self) -> X:
218
+ return self._i
219
+
220
+
221
+ class B(A[X], Generic[X]):
222
+ def __init__(self, i: X, j: X, /):
223
+ super().__init__(i)
224
+ self._j: X = j
225
+
226
+ @property
227
+ def j(self) -> X:
228
+ return self._j
229
+
230
+ def possible_problem(x: A) -> TypeIs[A[int]]:
231
+ return isinstance(x.i, int)
232
+
233
+ def possible_correction(x: A) -> TypeIs[A[int]]:
234
+ if type(x) is A:
235
+ # only narrow cases we know about
236
+ return isinstance(x.i, int)
237
+ raise TypeError(
238
+ f"Refusing to narrow Genenric type {type(x)!r}"
239
+ f"from function that only knows about {A!r}"
240
+ )
241
+
242
+ Because it is possible to attempt to narrow B,
243
+ but A does not have appropriate information about B
244
+ (or any other unknown subclass of A!) it's not possible to safely narrow
245
+ in either direction. The general rule for generics is that if you do not know
246
+ all the places a generic class is generic and do not check enough of them to be
247
+ absolutely certain, you cannot return True, and if you do not have a definitive
248
+ counter example to the type to be narrowed to you cannot return False.
249
+ In practice, if soundness is prioritized over an unsafe narrowing,
250
+ not knowing what you don't know is solvable by either
251
+ erroring out when neither return option is safe, or by making the class to be
252
+ narrowed final to avoid such a situation.
198
253
199
254
Safety and soundness
200
255
--------------------
201
256
202
257
While type narrowing is important for typing real-world Python code, many
203
- forms of type narrowing are unsafe in the presence of mutability . Type checkers
258
+ forms of type narrowing are unsafe. Type checkers
204
259
attempt to limit type narrowing in a way that minimizes unsafety while remaining
205
260
useful, but not all safety violations can be detected.
206
261
262
+ One example of this tradeoff building off of TypeIs
263
+
264
+ If you trust that users implementing the Sequence Protocol are doing so in a
265
+ way that is safe to iterate over, and will not be mutated for the duration
266
+ you are relying on it; then while the following function can never be fully
267
+ sound, full soundness is not necessarily easier or better for your use::
268
+
269
+ def useful_unsoundness(s: Sequence[object]) -> TypeIs[Sequence[int]]:
270
+ return all(isinstance(i, int) for i in s)
271
+
272
+ However, many cases of this sort can be extracted for safe use with an
273
+ alternative construction if soundness is of a high priority,
274
+ and the cost of a copy is acceptable::
275
+
276
+ def safer(s: Sequence[object]) -> Sequence[int]:
277
+ ret = tuple(i for i in s if isinstance(i, int))
278
+ if len(ret) != len(s):
279
+ raise TypeError
280
+ return ret
281
+
282
+
207
283
``isinstance() `` and ``issubclass() ``
208
284
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
209
285
0 commit comments