Skip to content

PEP-681 descriptor-typed field support misinterpreted: __init__ parameter type should be __set__ value type #21874

Description

@Jiankai-Sun

Title: PEP-681 descriptor-typed fields misinterpreted: init expects descriptor type instead of set value type

Minimal reproducible example (no third-party dependencies)

from __future__ import annotations

from typing import Any, Generic, Optional, TypeVar, Union, TYPE_CHECKING, overload
from typing_extensions import dataclass_transform

_T = TypeVar("_T")

def model_field(
    *,
    default: Optional[Any] = None,
    init: bool = True,
) -> Any:
    raise NotImplementedError()

@dataclass_transform(
    eq_default=True, order_default=True, field_specifiers=(model_field,)
)
class ModelBase:
    def __init_subclass__(
        cls,
        *,
        init: bool = True,
        frozen: bool = False,
        eq: bool = True,
        order: bool = True,
    ):
        ...

class Mapped(Generic[_T]):
    if TYPE_CHECKING:

        @overload
        def __get__(self, instance: None, owner: Any) -> Mapped[_T]:
            ...

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

        def __get__(
            self, instance: Optional[object], owner: Any
        ) -> Union[Mapped[_T], _T]:
            ...

        def __set__(self, instance: Any, value: _T) -> None:
            ...

        def __delete__(self, instance: Any) -> None:
            ...

class AlsoCustomer(ModelBase):
    a: Mapped[int]

# This is correct at runtime and under pyright
c2 = AlsoCustomer(a=5)

# This should be an error (and is under pyright)
c3 = AlsoCustomer(a="some string")

Actual behavior (mypy)

error: Argument "a" to "AlsoCustomer" has incompatible type "int"; expected "Mapped[int]"

Expected behavior

The synthesized init parameter for a descriptor-typed field (like a: Mapped[int]) should accept the value type of the descriptor's set method (here int), not the descriptor type itself (Mapped[int]). So AlsoCustomer(a=5) should type-check, and AlsoCustomer(a="some string") should be rejected.

This is the interpretation implemented by pyright and matches the runtime behavior of Python dataclasses when descriptors are used as field types.

Relevant specification

PEP-681, Descriptor-typed field support (non-normative note): "When enabled, the type of each parameter on the synthesized init method corresponding to a descriptor-typed field would be the type of the value parameter to the descriptor's set method rather than the descriptor type itself."

Background

Extensive discussion with PEP-681 authors and pyright maintainers reached consensus on this behavior (see microsoft/pyright#2958 and microsoft/pyright#3245). SQLAlchemy's Mapped descriptor relies on this correct interpretation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions