diff --git a/docs/spec/constructors.rst b/docs/spec/constructors.rst new file mode 100644 index 00000000..e0899678 --- /dev/null +++ b/docs/spec/constructors.rst @@ -0,0 +1,492 @@ +Constructors +============ + +Calls to constructors require special handling within type checkers. + +Constructor Calls +----------------- + +At runtime, a call to a class' constructor typically results in the invocation of +three methods in the following order: +1. The ``__call__`` method of the metaclass (which is typically supplied by the + ``type`` class but can be overridden by a custom metaclass and which is + responsible for calling the next two methods) +2. The ``__new__`` static method of the class +3. The ``__init__`` instance method of the class + +Type checkers should mirror this runtime behavior when analyzing a constructor +call. + +Metaclass ``__call__`` Method +============================= + +When evaluating a constructor call, a type checker should first check if the +class has a custom metaclass (a subclass of ``type``) that defines a ``__call__`` +method. If so, it should evaluate the call of this method using the supplied +arguments. If the metaclass is ``type``, this step can be skipped. + +If the evaluated return type of the ``__call__`` method indicates something +other than an instance of the class being constructed, a type checker should +assume that the metaclass ``__call__`` method is overriding ``type.__call__`` +in some special manner, and it should not attempt to evaluate the ``__new__`` +or ``__init__`` methods on the class. For example, some metaclass ``__call__`` +methods are annotated to return ``NoReturn`` to indicate that constructor +calls are not supported for that class. + + :: + + class Meta(type): + def __call__(cls, *args, **kwargs) -> NoReturn: + raise TypeError("Cannot instantiate class") + + class MyClass(metaclass=Meta): + def __new__(cls, *args, **kwargs) -> Self: + return super().__new__(cls, *args, **kwargs) + + assert_type(MyClass(), Never) + +If no return type annotation is provided for ``__call__``, a type checker may +assume that it does not override ``type.__call__`` in a special manner and +proceed as though the return type is an instance of the type specified by +the ``cls`` parameter. + + +``__new__`` Method +================== + +After the metaclass ``__call__`` method has been evaluated, a type checker +should evaluate the ``__new__`` method of the class (if applicable) using +the supplied arguments. This step should be skipped if the class does not +define a ``__new__`` method and does not inherit a ``__new__`` method from +a base class other than ``object``. + +If the class is generic and explicitly specialized, the type checker should +partially specialize the ``__new__`` method using the supplied type arguments. +If the class is not explicitly specialized, class-scoped type variables should +be solved using the supplied arguments passed to the constructor call. + + :: + + class MyClass[T]: + def __new__(cls, x: T) -> Self: + return super().__new__(cls) + + # Constructor calls for specialized classes + assert_type(MyClass[int](1), MyClass[int]) + assert_type(MyClass[float](1), MyClass[float]) + MyClass[int](1.0) # Type error + + # Constructor calls for non-specialized classes + assert_type(MyClass(1), MyClass[int]) + assert_type(MyClass(1.0), MyClass[float]) + +If any class-scoped type variables are not solved when evaluating the ``__new__`` +method call using the supplied arguments, these type variables should be left +unsolved, allowing the ``__init__`` method (if applicable) to be used to solve +them. + + :: + + class MyClass[T]: + def __new__(cls, *args, **kwargs) -> Self: + return super().__new__(cls) + + def __init__(self, x: T) -> None: + pass + + assert_type(MyClass(1), MyClass[int]) + assert_type(MyClass(""), MyClass[str]) + +For most classes, the return type for the ``__new__`` method is typically +``Self``, but other types are also allowed. For example, the ``__new__`` +method may return an instance of a subclass or an instance of some completely +unrelated class. + +If the evaluated return type of ``__new__`` is not the class being constructed +(or a subclass thereof), a type checker should assume that the ``__init__`` +method will not be called. This is consistent with the runtime behavior of +the ``type.__call__`` method. If the ``__new__`` method return type is +a union with one or more subtypes that are not instances of the class being +constructed (or a subclass thereof), a type checker should likewise assume that +the ``__init__`` method will not be called. + + :: + + class MyClass: + def __new__(cls) -> int: + return 0 + + # In this case, the __init__ method should not be considered + # by the type checker when evaluating a constructor call. + def __init__(self, x: int): + pass + + assert_type(MyClass(), int) + +For purposes of this test, an explicit return type of ``Any`` (or a +union containing ``Any``) should be treated as a type that is not an instance +of the class being constructed. + + :: + + class MyClass: + def __new__(cls) -> Any: + return 0 + + # The __init__ method will not be called in this case, so + # it should not be evaluated. + def __init__(self, x: int): + pass + + assert_type(MyClass(), Any) + +If the return type of ``__new__`` is not annotated, a type checker may assume +that the return type is ``Self`` and proceed with the assumption that the +``__init__`` method will be called. + +If the class is generic, it is possible for a ``__new__`` method to override +the specialized class type and return a class instance that is specialized +with different type arguments. + + :: + + class MyClass[T]: + def __new__(cls, *args, **kwargs) -> "MyClass[list[T]]": + ... + + assert_type(MyClass[int](), MyClass[list[int]]) + +If the ``cls`` parameter within the ``__new__`` method is not annotated, type +checkers should infer a type of ``type[Self]``. Regardless of whether the +type of the ``cls`` parameter is explicit or inferred, the type checker should +bind the class being constructed to the ``cls`` parameter and report any type +errors that arise during binding. + + :: + + class MyClass[T]: + def __new__(cls: "type[MyClass[int]]") -> "MyClass[int]": ... + + MyClass() # OK + MyClass[int]() # OK + MyClass[str]() # Type Error + + +``__init__`` Method +=================== + +After evaluating the ``__new__`` method, a type checker should evaluate the +``__init__`` method (if applicable) using the supplied arguments. If the class +is generic and explicitly specialized (or specialized via the ``__new__`` method +return type), the type checker should partially specialize the ``__init__`` +method using the supplied type arguments. If the class is not explicitly +specialized, class-scoped type variables should be solved using the supplied +arguments passed to the constructor call. + +This step should be skipped if the class does not define an ``__init__`` method +and does not inherit an ``__init__`` method from a base class other than +``object``. + + :: + + class MyClass[T]: + def __init__(self, x: T) -> None: + ... + + # Constructor calls for specialized classes + assert_type(MyClass[int](1), MyClass[int]) + assert_type(MyClass[float](1), MyClass[float]) + MyClass[int](1.0) # Type error + + # Constructor calls for non-specialized classes + assert_type(MyClass(1), MyClass[int]) + assert_type(MyClass(1.0), MyClass[float]) + +If the ``self`` parameter within the ``__init__`` method is not annotated, type +checkers should infer a type of ``Self``. Regardless of whether the ``self`` +parameter type is explicit or inferred, a type checker should bind the class +being constructed to this parameter and report any type errors that arise +during binding. + + :: + + class MyClass[T]: + def __init__(self: "MyClass[int]") -> None: ... + + MyClass() # OK + MyClass[int]() # OK + MyClass[str]() # Type Error + +The return type for ``__init__`` is always ``None``, which means the +method cannot influence the return type of the constructor call by specifying +a return type. There are cases where it is desirable for the ``__init__`` method +to influence the return type, especially when the ``__init__`` method is +overloaded. To enable this, type checkers should allow the ``self`` parameter +to be annotated with a type that influences the resulting type of the +constructor call. + + :: + + class MyClass1[T]: + @overload + def __init__(self: "MyClass1[list[int]]", value: int) -> None: ... + @overload + def __init__(self: "MyClass1[set[str]]", value: str) -> None: ... + @overload + def __init__(self, value: T) -> None: ... + + + assert_type(MyClass1(0), MyClass1[list[int]]) + assert_type(MyClass1[int](3), MyClass1[int]) + assert_type(MyClass1(""), MyClass1[set[str]]) + assert_type(MyClass1(3.0), MyClass1[float]) + + +Function-scoped type variables can also be used in the ``self`` +annotation of an ``__init__`` method to influence the return type of the +constructor call. + + :: + + class MyClass2[T1, T2]: + def __init__[V1, V2](self: "MyClass2[V1, V2]", value1: V1, value2: V2) -> None: ... + + assert_type(MyClass2(0, ""), MyClass2[int, str]) + assert_type(MyClass2[int, str](0, ""), MyClass2[int, str]) + + class MyClass3[T1, T2]: + def __init__[V1, V2](self: "MyClass3[V2, V1]", value1: V1, value2: V2) -> None: ... + + assert_type(MyClass3(0, ""), MyClass3[str, int]) + assert_type(MyClass3[str, int](0, ""), MyClass3[str, int]) + + +Class-scoped type variables should not be used in the ``self`` annotation +because such use can lead to ambiguous or nonsensical type evaluation results. +Type checkers should report an error if a class-scoped type variable is used +within a type annotation for the ``self`` parameter in an ``__init__`` method. + + :: + + class MyClass4[T1, T2]: + # The ``self`` annotation should result in a type error + def __init__(self: "MyClass4[T2, T1]") -> None: ... + + +Classes Without ``__new__`` and ``__init__`` Methods +==================================================== + +If a class does not define a ``__new__`` method or ``__init__`` method and +does not inherit either of these methods from a base class other than +``object``, a type checker should evaluate the argument list using the +``__new__`` and ``__init__`` methods from the ``object`` class. + + :: + + class MyClass5: + pass + + MyClass5() # OK + MyClass5(1) # Type error + + +Constructor Calls for type[T] +----------------------------- + +When a value of type ``type[T]`` (where ``T`` is a concrete class or a type +variable) is called, a type checker should evaluate the constructor call as if +it is being made on the class ``T`` (or the class that represents the upper bound +of type variable ``T``). This means the type checker should use the ``__call__`` +method of ``T``'s metaclass and the ``__new__`` and ``__init__`` methods of ``T`` +to evaluate the constructor call. + +It should be noted that such code could be unsafe because the type ``type[T]`` +may represent subclasses of ``T``, and those subclasses could redefine the +``__new__`` and ``__init__`` methods in a way that is incompatible with the +base class. Likewise, the metaclass of ``T`` could redefine the ``__call__`` +method in a way that is incompatible with the base metaclass. + + +Specialization During Construction +---------------------------------- + +As discussed above, if a class is generic and not explicitly specialized, its +type variables should be solved using the arguments passed to the ``__new__`` +and ``__init__`` methods. If one or more type variables are not solved during +these method evaluations, they should take on their default values. + + :: + + T1 = TypeVar("T1") + T2 = TypeVar("T2") + T3 = TypeVar("T3", default=str) + + class MyClass1(Generic[T1, T2]): + def __new__(cls, x: T1) -> Self: ... + + assert_type(MyClass1(1), MyClass1[int, Any]) + + class MyClass2(Generic[T1, T3]): + def __new__(cls, x: T1) -> Self: ... + + assert_type(MyClass2(1), MyClass2[int, str]) + + +Consistency of ``__new__`` and ``__init__`` +------------------------------------------- + +Type checkers may optionally validate that the ``__new__`` and ``__init__`` +methods for a class have consistent signatures. + + :: + + class MyClass: + def __new__(cls) -> Self: + return super().__new__(cls) + + # Type error: __new__ and __init__ have inconsistent signatures + def __init__(self, x: str) -> None: + pass + + +Converting a Constructor to Callable +------------------------------------ + +Class objects are callable, which means they are compatible with callable types. + + :: + + def accepts_callable[**P, R](cb: Callable[P, R]) -> Callable[P, R]: + return cb + + class MyClass: + def __init__(self, x: int) -> None: + pass + + reveal_type(accepts_callable(MyClass)) # ``def (x: int) -> MyClass`` + +When converting a class to a callable type, a type checker should use the +following rules, which reflect the same rules specified above for evaluating +constructor calls: + +1. If the class has a custom metaclass that defines a ``__call__`` method + that is annotated with a return type other than a subclass of the + class being constructed (or a union that contains such a type), a type + checker should assume that the metaclass ``__call__`` method is overriding + ``type.__call__`` in some special manner. In this case, the callable should + be synthesized from the parameters and return type of the metaclass + ``__call__`` method after it is bound to the class, and the ``__new__`` or + ``__init__`` methods (if present) should be ignored. This is an uncommon + case. In the more typical case where there is no custom metaclass that + overrides ``type.__call__`` in a special manner, the metaclass ``__call__`` + signature should be ignored for purposes of converting to a callable type. + If a custom metaclass ``__call__`` method is present but does not have an + annotated return type, type checkers may assume that the method acts like + ``type.__call__`` and proceed to the next step. + +2. If the class defines a ``__new__`` method or inherits a ``__new__`` method + from a base class other than ``object``, a type checker should synthesize a + callable from the parameters and return type of that method after it is bound + to the class. + +3. If the return type of the method in step 2 evaluates to a type that is not a + subclass of the class being constructed (or a union that includes such a + class), the final callable type is based on the result of step 2, and the + conversion process is complete. The ``__init__`` method is ignored in this + case. This is consistent with the runtime behavior of the ``type.__call__`` + method. + +4. If the class defines an ``__init__`` method or inherits an ``__init__`` method + from a base class other than ``object``, a callable type should be synthesized + from the parameters of the ``__init__`` method after it is bound to the class + instance resulting from step 2. The return type of this synthesized callable + should be the concrete value of ``Self``. + +5. If step 2 and 4 both produce no result because the class does not define or + inherit a ``__new__`` or ``__init__`` method from a class other than ``object``, + the type checker should synthesize callable types from the ``__new__`` and + ``__init__`` methods for the ``object`` class. + +6. Steps 2, 4 and 5 will produce either one or two callable types. The final + result of the conversion process is the union of these types. This will + reflect the callable signatures of the applicable ``__new__`` and + ``__init__`` methods. + + :: + + class A: + """ No __new__ or __init__ """ + pass + + class B: + """ __new__ and __init__ """ + def __new__(cls, *args, **kwargs) -> Self: + ... + + def __init__(self, x: int) -> None: + ... + + class C: + """ __new__ but no __init__ """ + def __new__(cls, x: int) -> int: + ... + + class CustomMeta(type): + def __call__(cls) -> NoReturn: + raise NotImplemented("Class not constructable") + + class D(metaclass=CustomMeta): + """ Custom metaclass that overrides type.__call__ """ + def __new__(cls, *args, **kwargs) -> Self: + """ This __new__ is ignored for purposes of conversion """ + pass + + + class E: + """ __new__ that causes __init__ to be ignored """ + + def __new__(cls) -> A: + return A.__new__() + + def __init__(self, x: int) -> None: + """ This __init__ is ignored for purposes of conversion """ + ... + + + reveal_type(accepts_callable(A)) # ``def () -> A`` + reveal_type(accepts_callable(B)) # ``def (*args, **kwargs) -> B | def (x: int) -> B`` + reveal_type(accepts_callable(C)) # ``def (x: int) -> int`` + reveal_type(accepts_callable(D)) # ``def () -> NoReturn`` + reveal_type(accepts_callable(E)) # ``def () -> A`` + + +If the ``__init__`` or ``__new__`` method is overloaded, the callable +type should be synthesized from the overloads. The resulting callable type +itself will be overloaded. + + :: + + class MyClass: + @overload + def __init__(self, x: int) -> None: ... + @overload + def __init__(self, x: str) -> None: ... + + reveal_type(accepts_callable(MyClass)) # overload of ``def (x: int) -> MyClass`` and ``def (x: str) -> MyClass`` + + +If the class is generic, the synthesized callable should include any class-scoped +type parameters that appear within the signature, but these type parameters should +be converted to function-scoped type parameters for the callable. +Any function-scoped type parameters in the ``__init__`` or ``__new__`` +method should also be included as function-scoped type parameters in the synthesized +callable. + + :: + + class MyClass[T]: + def __init__[V](self, x: T, y: list[V], z: V) -> None: ... + + reveal_type(accepts_callable(MyClass)) # ``def [T, V] (x: T, y: list[V], z: V) -> MyClass[T]`` + + diff --git a/docs/spec/index.rst b/docs/spec/index.rst index 6da9710e..3e77898c 100644 --- a/docs/spec/index.rst +++ b/docs/spec/index.rst @@ -17,6 +17,7 @@ Specification for the Python type system literal protocol callables + constructors overload dataclasses typeddict