Skip to content

Incorrect type narrowing with subclassed descriptors #11039

@olavmo-sikt

Description

@olavmo-sikt

Describe the bug

I was looking at why a VS code refused to tab-complete a property when working with a SQLAlchemy ORM object:

import typing
import sqlalchemy.orm

class Base(sqlalchemy.orm.DeclarativeBase):
    pass

class User(Base):
    email: sqlalchemy.orm.MappedColumn[str | None]

u = User()
typing.reveal_type(u)  # Type of "u" is "User"
typing.reveal_type(u.email)  # Type of "u.email" is "str | None"
assert u.email is None
typing.reveal_type(u)  # Type of "u" is "Never"
# VS code now refuses to tab-complete any attributes on `u.`

After simplifying the relevant classes, it looks like the problem is that MappedColumn indirectly subclasses the ORMDescriptor class. For some reason, the type of the object is narrowed to typing.Never when the descriptor class is accessed through a subclass, instead of directly.

Code or Screenshots

The following code reproduces the problem in the Pyright playground:

import typing

T = typing.TypeVar("T", bound=typing.Any)

class SQLCoreOperations(typing.Generic[T]):
    pass

class ORMDescriptor(typing.Generic[T]):  # Simplified from https://github.com/sqlalchemy/sqlalchemy/blob/rel_2_0_44/lib/sqlalchemy/orm/base.py#L702-L725
    @typing.overload
    def __get__(
        self, instance: typing.Any, owner: typing.Literal[None]
    ) -> "ORMDescriptor[T]": ...

    @typing.overload
    def __get__(
        self, instance: typing.Literal[None], owner: typing.Any
    ) -> SQLCoreOperations[T]: ...

    @typing.overload
    def __get__(self, instance: object, owner: typing.Any) -> T: ...

    def __get__(
        self, instance: object, owner: typing.Any
    ) -> "typing.Union[ORMDescriptor[T], SQLCoreOperations[T], T]": ...

class MappedColumn(ORMDescriptor[T]):
    pass

class User:
    email: MappedColumn[str | None]

u = User()
typing.reveal_type(u)  # Type of "u" is "User"
typing.reveal_type(u.email)  # Type of "u.email" is "str | None"
assert u.email is None
typing.reveal_type(u)  # Type of "u" is "Never"

(Link to code in Pyright Playground)

If email: MappedColumn[str | None] is replaced with email: ORMDescriptor[str | None], u is no longer narrowed to typing.Never:

# [...]
class User:
    email: ORMDescriptor[str | None]

u = User()
typing.reveal_type(u)  # Type of "u" is "User"
typing.reveal_type(u.email)  # Type of "u.email" is "str | None"
assert u.email is None
typing.reveal_type(u)  # Type of "u" is "User"

(Link to full code in Pyright Playground)

VS Code extension or command-line

Issue reproduced in Pyright playground using Pyright version 1.1.406.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions