Should literal tuple membership test narrow the type?

Should narrowing3 type check? If not, why? (Tested with mypy/pyright).

One workaround is using the approach in narrowing2 but it is verbose and e.g. ruff wants to nudge you to rewrite it.

from typing import Literal

from pydantic import BaseModel


class A(BaseModel):
    type: Literal["a"] = "a"
    value: int


class B(BaseModel):
    type: Literal["b"] = "b"
    value: int


class C(BaseModel):
    type: Literal["c"] = "c"


def narrowing1(model: A | B | C) -> None:
    if model.type == "a":
        model.value # OK

def narrowing2(model: A | B | C) -> None:
    if model.type == "a" or model.type == "b":  #OK for mypy/pyright but Ruff wants to rewrite it.
        model.value  # OK

def narrowing3(model: A | B | C) -> None:
    if model.type in ("a", "b"):
        model.value # :( Item "C" of "A | B | C" has no attribute "value"  [union-attr]
1 Like

Specific type narrowing behaviors are not specified or mandated in the typing spec, so you’ll see different behaviors from different tools.

I don’t think that mypy has a documented list of type guard forms that it supports, but you can find such a list for pyright here. Your code in narrowing1 and narrowing2 maps to the type guard pattern x.y == LN. The code in narrowing3 does not map to any of the supported forms.

In general, each supported type guard form requires specialized logic in a type checker. There’s an infinite number of potential forms, so it’s not practical to support them all. Type checkers typically implement support for the most common forms. For unsupported forms, you can typically write your own user-defined type guard function
using TypeGuard or TypeIs.

3 Likes

Thank you. The pyright docs are great, btw. I wish they came up more often also on organic python typing searches.

I wonder if this form would warrant being supported. Personally I have run into it a few times, but maybe it is a rare thing.

That decision would be up to each type checker.

For pyright, I would typically defer such an enhancement request until I saw good evidence for it being a common usage pattern. I did a quick search of the pyright and mypy issue trackers, and I found requests for several dozen other type guard forms (each with varying numbers of upvotes), but I couldn’t find any requests for this pattern. That would seem to indicate that it’s not a common use case.

1 Like

And this issue attracted multiple people wanting something similar and getting confused (which you correctly redirected to other places).

So I do think there is repeated requests for something like this, especially if you check the mypy issue and see that multiple issues have been closed as duplicates of it.

Unless I am misunderstanding what you mean with type guard here?

@MegaIng, that’s a different form. Pyright already supports the form x in y (where y is one of several stdlib container types).

def func(x: str):
    assert x in ("a", "b", "c")
    reveal_type(x)  # Pyright: Literal['a', 'b', 'c'], Mypy: str

Roderick is requesting support for a different form that handles discriminated unions based on an attribute access (x.y in <literal tuple of literal strings>). I wasn’t able to find any existing requests for this form.

Aha, alright. I would have thought that the forms for x.y are derived based on the forms for x, but I guess not. I see the explicit listings now.