Skip to content

Commit cc8a4b5

Browse files
authored
Document we're not tracking relationships between symbols (#16018)
Fixes #15653. I did not use erictraut's "quantum entanglement" metaphor, though I find it to be quite illustrative :)
1 parent 6a6d2e8 commit cc8a4b5

File tree

1 file changed

+37
-5
lines changed

1 file changed

+37
-5
lines changed

docs/source/type_narrowing.rst

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Type narrowing
44
==============
55

6-
This section is dedicated to several type narrowing
6+
This section is dedicated to several type narrowing
77
techniques which are supported by mypy.
88

99
Type narrowing is when you convince a type checker that a broader type is actually more specific, for instance, that an object of type ``Shape`` is actually of the narrower type ``Square``.
@@ -14,10 +14,11 @@ Type narrowing expressions
1414

1515
The simplest way to narrow a type is to use one of the supported expressions:
1616

17-
- :py:func:`isinstance` like in ``isinstance(obj, float)`` will narrow ``obj`` to have ``float`` type
18-
- :py:func:`issubclass` like in ``issubclass(cls, MyClass)`` will narrow ``cls`` to be ``Type[MyClass]``
19-
- :py:class:`type` like in ``type(obj) is int`` will narrow ``obj`` to have ``int`` type
20-
- :py:func:`callable` like in ``callable(obj)`` will narrow object to callable type
17+
- :py:func:`isinstance` like in :code:`isinstance(obj, float)` will narrow ``obj`` to have ``float`` type
18+
- :py:func:`issubclass` like in :code:`issubclass(cls, MyClass)` will narrow ``cls`` to be ``Type[MyClass]``
19+
- :py:class:`type` like in :code:`type(obj) is int` will narrow ``obj`` to have ``int`` type
20+
- :py:func:`callable` like in :code:`callable(obj)` will narrow object to callable type
21+
- :code:`obj is not None` will narrow object to its :ref:`non-optional form <strict_optional>`
2122

2223
Type narrowing is contextual. For example, based on the condition, mypy will narrow an expression only within an ``if`` branch:
2324

@@ -83,6 +84,7 @@ We can also use ``assert`` to narrow types in the same context:
8384
reveal_type(x) # Revealed type is "builtins.int"
8485
print(x + '!') # Typechecks with `mypy`, but fails in runtime.
8586
87+
8688
issubclass
8789
~~~~~~~~~~
8890

@@ -359,3 +361,33 @@ What happens here?
359361
.. note::
360362

361363
The same will work with ``isinstance(x := a, float)`` as well.
364+
365+
Limitations
366+
-----------
367+
368+
Mypy's analysis is limited to individual symbols and it will not track
369+
relationships between symbols. For example, in the following code
370+
it's easy to deduce that if :code:`a` is None then :code:`b` must not be,
371+
therefore :code:`a or b` will always be a string, but Mypy will not be able to tell that:
372+
373+
.. code-block:: python
374+
375+
def f(a: str | None, b: str | None) -> str:
376+
if a is not None or b is not None:
377+
return a or b # Incompatible return value type (got "str | None", expected "str")
378+
return 'spam'
379+
380+
Tracking these sort of cross-variable conditions in a type checker would add significant complexity
381+
and performance overhead.
382+
383+
You can use an ``assert`` to convince the type checker, override it with a :ref:`cast <casts>`
384+
or rewrite the function to be slightly more verbose:
385+
386+
.. code-block:: python
387+
388+
def f(a: str | None, b: str | None) -> str:
389+
if a is not None:
390+
return a
391+
elif b is not None:
392+
return b
393+
return 'spam'

0 commit comments

Comments
 (0)