Skip to content

Commit

Permalink
Fix cross-variable type-narrowing example (#17488)
Browse files Browse the repository at this point in the history
From [<i>Type narrowing</i> &sect;
<i>Limitations</i>](https://github.com/python/mypy/blob/606971807fad1de26ebc575d327d4c1c33f71c0e/docs/source/type_narrowing.rst#limitations):

```python
def f(a: str | None, b: str | None) -> str:
    if a is not None or b is not None:
        return a or b  # Incompatible return value type (got "str | None", expected "str")
    return 'spam'
```

A trivial counter-example is `f('', None)`, which returns `None`.
Ironically, this somewhat makes Mypy's diagnostic "correct".

I propose that `str` be replaced with a custom class `C` whose
`__bool__()` is not defined (does it have to be `@final` too?):

```python
class C:
    pass

def f(a: C | None, b: C | None) -> C:
    if a is not None or b is not None:
        return a or b  # Incompatible return value type (got "C | None", expected "C")
    return C()
```
  • Loading branch information
InSyncWithFoo committed Jul 6, 2024
1 parent 1ea8676 commit 4ccf216
Showing 1 changed file with 10 additions and 6 deletions.
16 changes: 10 additions & 6 deletions docs/source/type_narrowing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -368,14 +368,18 @@ Limitations
Mypy's analysis is limited to individual symbols and it will not track
relationships between symbols. For example, in the following code
it's easy to deduce that if :code:`a` is None then :code:`b` must not be,
therefore :code:`a or b` will always be a string, but Mypy will not be able to tell that:
therefore :code:`a or b` will always be an instance of :code:`C`,
but Mypy will not be able to tell that:

.. code-block:: python
def f(a: str | None, b: str | None) -> str:
class C:
pass
def f(a: C | None, b: C | None) -> C:
if a is not None or b is not None:
return a or b # Incompatible return value type (got "str | None", expected "str")
return 'spam'
return a or b # Incompatible return value type (got "C | None", expected "C")
return C()
Tracking these sort of cross-variable conditions in a type checker would add significant complexity
and performance overhead.
Expand All @@ -385,9 +389,9 @@ or rewrite the function to be slightly more verbose:

.. code-block:: python
def f(a: str | None, b: str | None) -> str:
def f(a: C | None, b: C | None) -> C:
if a is not None:
return a
elif b is not None:
return b
return 'spam'
return C()

0 comments on commit 4ccf216

Please sign in to comment.