Implicit narrowing for immutable containers? (`tuple`, `NamedTuple`, frozen dataclass)

It would be kind of cool / convenient if immutable containers, specifically tuple, NamedTuple and frozen dataclasses (with standard __init__), would automatically narrow field types based on their arguments. ty and pyright already do this for standard tuples; and I realized one can emulate the desired effect for NamedTuple / frozen dataclasses by introducing type variables:

from typing import NamedTuple

r: tuple[int, int, int | None] = (1,2,3)
reveal_type(r[2])  # ty, pyright: Literal[3], other: int | None

class Point(NamedTuple):
    x: int
    y: int
    z: int | None
    
p = Point(1, 2, 3)
reveal_type(p.z)  # all: int | None

class CleverPoint[Z: int | None](NamedTuple):
    x: int
    y: int
    z: Z
    
q = CleverPoint(1, 2, 3)
reveal_type(q.z)  # ty: Literal[3], other: int

Idea: type checkers could implicitly treat a NamedTuple

class NTuple(NamedTuple):
    field1: type1
    field2: type2
    ...
    fieldN: typeN

as if it were:

class NTuple[T1: type1, T2: type2, ..., TN: typeN](NamedTuple):
    field1: T1
    field2: T2
    ...
    fieldN: TN

I believe due to covariance this should generally be safe?!

2 Likes

This makes sense for tuples because they’re internally in the type checker already represented as tuple[T1, T2, T3] with obvious slots where the narrowed type goes, but that isn’t the case for NamedTuples. An additional problem is that NamedTuples can also be explicitly generic.

So, I think this is sound, but I can also see why type checkers might be hesitant to implement this.

What you describe actually reminds me a lot of what Eric Traut described here on how pyright deals with untyped classes: Pseudo-generic classes to improve inference in untyped code · Issue #833 · astral-sh/ty · GitHub