Skip to content

Commit 3b1983e

Browse files
committed
retooled, moved to guides
1 parent 75caed7 commit 3b1983e

File tree

1 file changed

+77
-1
lines changed

1 file changed

+77
-1
lines changed

docs/guides/type_narrowing.rst

+77-1
Original file line numberDiff line numberDiff line change
@@ -195,15 +195,91 @@ This behavior can be seen in the following example::
195195
else:
196196
reveal_type(x) # Unrelated
197197

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.
198253

199254
Safety and soundness
200255
--------------------
201256

202257
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
204259
attempt to limit type narrowing in a way that minimizes unsafety while remaining
205260
useful, but not all safety violations can be detected.
206261

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+
207283
``isinstance()`` and ``issubclass()``
208284
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
209285

0 commit comments

Comments
 (0)