From bab532e6862db25e1d568171d350265d0a5efb31 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 13 Aug 2024 17:06:03 -0600 Subject: [PATCH 01/14] First draft of an update to the Overloads chapter. * Attempts to clearly define the algorithm for overload matching. * Describes checks for overload consistency, overlapping overloads, and implementation consistency. --- docs/spec/overload.rst | 406 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 392 insertions(+), 14 deletions(-) diff --git a/docs/spec/overload.rst b/docs/spec/overload.rst index 3b534046d..5e346f2e8 100644 --- a/docs/spec/overload.rst +++ b/docs/spec/overload.rst @@ -1,11 +1,21 @@ .. _`overload`: -``@overload`` +``Overloads`` ============= +In Python, it is common for callable objects to be polymorphic, meaning +they accept different types of arguments. It is also common for such +callables to return different types depending on the arguments passed to +them. Overloads provide a way to describe the accepted input signatures +and corresponding return types. + + +Overload definitions +^^^^^^^^^^^^^^^^^^^^ + The ``@overload`` decorator allows describing functions and methods -that support multiple different combinations of argument types. This -pattern is used frequently in builtin modules and types. For example, +that support multiple different combinations of argument types. This +pattern is used frequently in builtin modules and types. For example, the ``__getitem__()`` method of the ``bytes`` type can be described as follows:: @@ -18,9 +28,9 @@ follows:: @overload def __getitem__(self, s: slice) -> bytes: ... -This description is more precise than would be possible using unions -(which cannot express the relationship between the argument and return -types):: +This description is more precise than would be possible using unions, +which cannot express the relationship between the argument and return +types:: class bytes: ... @@ -54,15 +64,15 @@ Note that we could also easily add items to support ``map(None, ...)``:: iter2: Iterable[T2]) -> Iterable[tuple[T1, T2]]: ... Uses of the ``@overload`` decorator as shown above are suitable for -stub files. In regular modules, a series of ``@overload``-decorated +stub files. In regular modules, a series of ``@overload``-decorated definitions must be followed by exactly one non-``@overload``-decorated definition (for the same function/method). The ``@overload``-decorated definitions are for the benefit of the type checker only, since they will be overwritten by the non-``@overload``-decorated definition, while the latter is used at -runtime but should be ignored by a type checker. At runtime, calling -a ``@overload``-decorated function directly will raise -``NotImplementedError``. Here's an example of a non-stub overload +runtime but should be ignored by a type checker. At runtime, calling +an ``@overload``-decorated function directly will raise +``NotImplementedError``. Here's an example of a non-stub overload that can't easily be expressed using a union or a type variable:: @overload @@ -77,9 +87,9 @@ that can't easily be expressed using a union or a type variable:: def utf8(value): -A constrained ``TypeVar`` type can often be used instead of using the -``@overload`` decorator. For example, the definitions of ``concat1`` -and ``concat2`` in this stub file are equivalent:: +A constrained ``TypeVar`` type can sometimes be used instead of +using the ``@overload`` decorator. For example, the definitions +of ``concat1`` and ``concat2`` in this stub file are equivalent:: from typing import TypeVar @@ -99,8 +109,376 @@ variable is not sufficient. Another important difference between type variables such as ``AnyStr`` and using ``@overload`` is that the prior can also be used to define -constraints for generic class type parameters. For example, the type +constraints for generic class type parameters. For example, the type parameter of the generic class ``typing.IO`` is constrained (only ``IO[str]``, ``IO[bytes]`` and ``IO[Any]`` are valid):: class IO(Generic[AnyStr]): ... + + +Invalid overload definitions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Type checkers should enforce the following rules for overload definitions. + +At least two ``@overload``-decorated definitions must be present. If only +one is present, an error should be reported. + +An overload implementation should be present for all overload function +definitions. Type checkers should report an error or warning if an +implementation is missing. Overload definitions within stub files, +protocols, and abstract base classes are exempt from this check. + +If an overload is decorated with ``@staticmethod`` or ``@classmethod``, +all overloads must be similarly decorated. The implementation, +if present, must be decorated in the same manner. Similarly, if one overload +is ``async``, all overloads must be ``async`` as well as the implementation. +Type checkers should report an error if these conditions are not met. + +If one or more overloads are decorated with ``@final`` but the +implementation is not, an error should be reported. + + +Implementation consistency +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If an overload implementation is defined, type checkers should validate +that its signature is consistent with all of its associated overload +signatures. The implementation should accept all potential argument lists +that are accepted by the overloads and should produce all potential return +types produced by the overloads. In typing terms, this means the input +signature of the implementation should be assignable to the input signatures +of all overloads, and the return type of all overloads should be assignable to +the return type of the implementation. + +If the implementation is inconsistent with its overloads, a type checker +should report an error:: + + @overload + def func(x: str, /) -> str: ... + @overload + def func(x: int) -> int: ... + + # This implementation is inconsistent with the second overload + # because it does not accept a keyword argument ``x`` and the + # the overload's return type ``int`` is not assignable to the + implementation's return type ``str``. + def func(x: int | str, /) -> str: + return str(x) + +Overlapping overloads +^^^^^^^^^^^^^^^^^^^^^ + +If two overloads can accept the same set of arguments but have +different return types, the overloads are said to "partially overlap". +This condition is indicative of a programming error and should +be reported by type checkers:: + + # These overloads partially overlap because both accept an + # argument of type ``Literal[0]``, but their return types + # differ. + + @overload + def func(x: Literal[0]) -> int: ... + @overload + def func(x: int) -> str: ... + +If all arguments accepted by an overload are also always accepted by +an earlier overload, the two overloads are said to "fully overlap". +In this case, the latter overload will never be used. This condition +is indicative of a programming error and should be reported by type +checkers:: + + # These overloads fully overlap because the second overload + # accepts all arguments accepted by the first overload. + + @overload + def func[T](x: T) -> T: ... + @overload + def func(x: int) -> int: ... + + +Overload call evaluation +^^^^^^^^^^^^^^^^^^^^^^^^ + +When a type checker evaluates the call of an overloaded function, it +attempts to "match" the supplied arguments with one or more overloads. +This section describes the algorithm that type checkers should use +for overload matching. + +Only the overloads (the ``@overload``-decorated signatures) should be +considered for matching purposes. The implementation, if provided, +should be ignored for purposes of overload matching. + + +Step 1: Examine the argument list to determine the number of +positional and keyword arguments. Use this information to eliminate any +overload candidates that are not plausible based on their +input signatures. + +- If no candidate overloads remain, generate an error and stop. +- If only one candidate overload remains, it is the winning match. Evaluate +it as if it were a non-overloaded function call and stop. +- If two or more candidate overloads remain, proceed to step 2. + + +Step 2: Evaluate each remaining overload as a regular (non-overloaded) +call to determine whether it is compatible with the supplied +argument list. Unlike step 1, this step considers the types of the parameters +and arguments. During this step, do not generate any user-visible errors. +Simply record which of the overloads result in evaluation errors. + +- If all overloads result in errors, proceed to step 3. +- If only one overload evaluates without error, it is the winning match. +Evaluate it as if it were a non-overloaded function call and stop. +- If two or more candidate overloads remain, proceed to step 4. + + +Step 3: If step 2 produces errors for all overloads, perform +"argument type expansion". Some types can be decomposed +into two or more subtypes. For example, the type ``int | str`` can be +expanded into ``int`` and ``str``. + +Expansion should be performed one argument at a time from left to +right. Each expansion results in sets of effective argument types. +For example, if there are two arguments whose types evaluate to +``int | str`` and ``int | bytes``, expanding the first argument type +results in two sets of argument types: ``(int, ?)`` and ``(str, ?)``. +Here ``?`` represents an unexpanded argument type. +If expansion of the second argument is required, four sets of +argument types are produced: ``(int, int)``, ``(int, bytes)``, +``(str, int)``, and ``(str, bytes)``. + +After each argument expansion, return to step 2 and evaluate all +expanded argument lists. + +- If all argument lists evaluate successfully, combine their +respective return types by union to determine the final return type +for the call, and stop. +- If argument expansion has been applied to all arguments and one or +more of the expanded argument lists cannot be evaluated successfully, +generate an error and stop. + +For additional details about argument type expansion, see +:ref:`argument-type-expansion` below. + + +Step 4: If the argument list is compatible with two or more overloads, +determine whether one or more of the overloads has a variadic parameter +(either ``*args`` or ``**kwargs``) that maps to a corresponding argument +that supplies an indeterminate number of positional or keyword arguments. +If so, eliminate overloads that do not have a variadic parameter. + +- If this results in only one remaining candidate overload, it is +the winning match. Evaluate it as if it were a non-overloaded function +call and stop. +- If two or more candidate overloads remain, proceed to step 5. + + +Step 5: If the type of one or more arguments evaluates to a +:ref:`gradual type` (e.g. ``list[Any]`` or ``str | Any``), determine +whether some theoretical :ref:`materialization` of these gradual types +could be used to disambiguate between two or more of the remaining +overloads. + +- If none of the arguments evaluate to a gradual type, proceed to step 6. +- If one or more arguments evaluate to a gradual type but no possible +materializations of these types would disambiguate between the remaining +overloads, proceed to step 6. +- If possible materializations of these types would disambiguate between +two or more of the remaining overloads and this subset of overloads have +consistent return types, proceed to step 6. If the return types include +type variables, constraint solving should be applied here before testing +for consistency. +- If none of the above conditions are met, the presence of gradual types +leads to an ambiguous overload selection. Assume a return type of ``Any`` +and stop. This preserves the "gradual guarantee". + +[Eric's note for reviewers: I'm struggling to come up with an +understandable and unambiguous way to describe this step. +Suggestions are welcome.] + +[Eric's note for reviewers: Pyright currently does not use return type +consistency in the above check. Instead, it looks for non-overlapping +return types. If return types are overlapping (that is, one is a consistent +subtype of another), it uses the wider return type. Only if there is no +consistency relationship between return types does it consider it an +"ambiguous" situation and turns it into an Any. This produces better +results for users of language servers, but it doesn't strictly preserve +the gradual guarantee. I'm willing to abandon this in favor of a +strict consistency check.] + + +Step 6: Choose the first remaining candidate overload as the winning +match. Evaluate it as if it were a non-overloaded function call and stop. + + +Example 1:: + + @overload + def example1(x: int, y: str) -> int: ... + @overload + def example1(x: str) -> str: ... + + example1() # Error in step 1: no plausible overloads + example1(1, "") # Step 1 eliminates second overload + example1("") # Step 1 eliminates first overload + + example1("", "") # Error in step 2: no compatible overloads + example1(1) # Error in step 2: no compatible overloads + + +Example 2:: + + @overload + def example2(x: int, y: str, z: int) -> str: ... + @overload + def example2(x: int, y: int, z: int) -> int: ... + + def test(val: str | int): + # In this example, argument type expansion is + # performed on the first two arguments. Expansion + # of the third is unnecessary. + r1 = example2(1, val, 1) + reveal_type(r1) # Should reveal str | int + + # Here, the types of all three arguments are expanded + # without success. + example2(val, val, val) # Error in step 3 + + +Example 3:: + + @overload + def example3(x: int) -> int: ... + @overload + def example3(x: int, y: int) -> tuple[int, int]: ... + @overload + def example3(*args: int) -> tuple[int, ...]: ... + + def test(): + # Step 1 eliminates second overload. Step 4 and + # step 5 do not apply. Step 6 picks the first + # overload. + r1 = example3(1) + reveal_type(r1) # Should reveal int + + # Step 1 eliminates first overload. Step 4 and + # step 5 do not apply. Step 6 picks the second + # overload. + r2 = example3(1, 2) + reveal_type(r2) # Should reveal tuple[int, int] + + # Step 1 doesn't eliminate any overloads. Step 4 + # picks the third overload. + val = [1, 2, 3, 4] + r3 = example3(*val) + reveal_type(r3) # Should reveal tuple[int, ...] + + +Example 4:: + + @overload + def example4(x: list[int], y: int) -> int: ... + @overload + def example4(x: list[str], y: str) -> int: ... + @overload + def example4(x: int, y: int) -> list[int]: ... + + def test(v1: list[Any], v2: Any): + # Step 4 eliminates third overload. Step 5 + # determines that first and second overloads + # both apply and are ambiguous due to Any, but + # return types are consistent. + r1 = example4(v1, 1) + reveal_type(r1) # Should reveal int + + # Step 4 eliminates third overload. Step 5 + # determines that first and second overloads + # both apply and are ambiguous due to Any, but + # return types are consistent. + r2 = example4([1], v2) + reveal_type(r2) # Should reveal int + + # Step 5 determines that first, second, and third + # overloads all apply and are ambiguous due to Any. + # These have inconsistent return types, so evaluated + # type is Any. + r3 = example4(v2, v2) + reveal_type(r3) # Should reveal Any + + + +Argument type expansion +^^^^^^^^^^^^^^^^^^^^^^^ + +When performing argument type expansion, the following types should be +expanded: + +1. Unions: Each subtype of the union should be considered as a separate +argument type. For example, the type ``int | str`` should be expanded +into ``int`` and ``str``. + +2. ``bool`` should be expanded into ``Literal[True]`` and ``Literal[False]``. + +3. ``Enum`` types (other than those that derive from ``enum.Flag``) should +be expanded into their literal members. + +4. ``type[A | B]`` should be expanded into ``type[A]`` and ``type[B]``. + +5. Tuples of known length that contain expandable types should be expanded +into all possible combinations of their subtypes. For example, the type +``tuple[int | str, bool]`` should be expanded into ``(int, Literal[True])``, +``(int, Literal[False])``, ``(str, Literal[True])``, and +``(str, Literal[False])``. + + +[Eric's note for reviewers: I'm not 100% convinced we should +support argument expansion in all of these cases. Tuple expansion, +in particular, can be very costly and can quickly blow up in complexity. +Currently, pyright and mypy support only the case 1 in the list above, +but I have had requests to support 2 and 3.] + +When performing type expansion for an argument, the argument that +is targeted for expansion should be evaluated without the use of +any context. All arguments that are not yet expanded should +continue to be evaluated with the benefit of context supplied by parameter +types within each overload signature. + +Example:: + + class MyDict[T](TypedDict): + x: T + + @overload + def func[T](a: int, b: MyDict[T]) -> T: ... + + @overload + def func(a: str, b: dict[str, int]) -> str: ... + + + def test(val: int | str): + result = func(val, {'x': 1}) + reveal_type(result) # Should reveal "int | str" + +In this case, type expansion is performed on the first argument, +which expands its type from ``int | str`` to ``int`` and ``str``. +The expression for the second argument is evaluated in the context +of both overloads. For the first overload, the second argument evaluates +to ``MyDict[int]``, and for the second overload it evaluates to +``dict[str, int]``. Both overloads are used to evaluate this call, +and the final type of ``result`` is ``int | str``. + +[Eric's note: mypy apparently doesn't do this currently. It evaluates all +arguments without the benefit of context, which produces less-than-ideal +results in some cases.] + + +[Eric's note for reviewers: We may want to provide for argument type expansion +for regular (non-overloaded) calls as well. This came up recently in +[this thread](https://discuss.python.org/t/proposal-relax-un-correlated-constrained-typevars/59658). +I'm a bit hesitant to add this to the spec because it adds significant +complexity to call evaluations and would likely result in a measurable slowdown +in type evaluation, but it's worth considering. We could perhaps mitigate the +slowdown by applying this behavior only when a constrained type variable is +used in the call's signature.] From de81026ce28282961524aaf3df662c4ce1c70495 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 23:07:10 +0000 Subject: [PATCH 02/14] [pre-commit.ci] auto fixes from pre-commit.com hooks --- docs/spec/overload.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/spec/overload.rst b/docs/spec/overload.rst index 5e346f2e8..7c298135f 100644 --- a/docs/spec/overload.rst +++ b/docs/spec/overload.rst @@ -158,7 +158,7 @@ should report an error:: def func(x: str, /) -> str: ... @overload def func(x: int) -> int: ... - + # This implementation is inconsistent with the second overload # because it does not accept a keyword argument ``x`` and the # the overload's return type ``int`` is not assignable to the @@ -169,7 +169,7 @@ should report an error:: Overlapping overloads ^^^^^^^^^^^^^^^^^^^^^ -If two overloads can accept the same set of arguments but have +If two overloads can accept the same set of arguments but have different return types, the overloads are said to "partially overlap". This condition is indicative of a programming error and should be reported by type checkers:: @@ -184,7 +184,7 @@ be reported by type checkers:: def func(x: int) -> str: ... If all arguments accepted by an overload are also always accepted by -an earlier overload, the two overloads are said to "fully overlap". +an earlier overload, the two overloads are said to "fully overlap". In this case, the latter overload will never be used. This condition is indicative of a programming error and should be reported by type checkers:: @@ -355,7 +355,7 @@ Example 3:: def example3(x: int, y: int) -> tuple[int, int]: ... @overload def example3(*args: int) -> tuple[int, ...]: ... - + def test(): # Step 1 eliminates second overload. Step 4 and # step 5 do not apply. Step 6 picks the first From 5ca254e5937265f101870f5319d55e0181219666 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 16 Aug 2024 11:55:46 -0700 Subject: [PATCH 03/14] Updated draft based on initial round of feedback. --- docs/spec/overload.rst | 126 ++++++++++++++++++++++++----------------- 1 file changed, 73 insertions(+), 53 deletions(-) diff --git a/docs/spec/overload.rst b/docs/spec/overload.rst index 5e346f2e8..dace0f383 100644 --- a/docs/spec/overload.rst +++ b/docs/spec/overload.rst @@ -124,27 +124,32 @@ Type checkers should enforce the following rules for overload definitions. At least two ``@overload``-decorated definitions must be present. If only one is present, an error should be reported. -An overload implementation should be present for all overload function -definitions. Type checkers should report an error or warning if an -implementation is missing. Overload definitions within stub files, -protocols, and abstract base classes are exempt from this check. +The ``@overload``-decorated definitions must be followed by an overload +implementation, which does not include an ``@overload`` decorator. Type +checkers should report an error or warning if an implementation is missing. +Overload definitions within stub files, protocols, and abstract base classes +are exempt from this check. If an overload is decorated with ``@staticmethod`` or ``@classmethod``, all overloads must be similarly decorated. The implementation, -if present, must be decorated in the same manner. Similarly, if one overload -is ``async``, all overloads must be ``async`` as well as the implementation. -Type checkers should report an error if these conditions are not met. +if present, must be decorated in the same manner. Type checkers should report +an error if these conditions are not met. -If one or more overloads are decorated with ``@final`` but the +If one or more overloads are decorated with ``@final`` or ``@override`` but the implementation is not, an error should be reported. +Overloads are allowed to use a mixture of ``async def`` and ``def`` statements +within the same overload definition. Type checkers should desugar all +``async def`` statements before testing for implementation consistency +and overlapping overloads (described below). + Implementation consistency ^^^^^^^^^^^^^^^^^^^^^^^^^^ If an overload implementation is defined, type checkers should validate -that its signature is consistent with all of its associated overload -signatures. The implementation should accept all potential argument lists +that it is consistent with all of its associated overload signatures. +The implementation should accept all potential sets of arguments that are accepted by the overloads and should produce all potential return types produced by the overloads. In typing terms, this means the input signature of the implementation should be assignable to the input signatures @@ -166,22 +171,36 @@ should report an error:: def func(x: int | str, /) -> str: return str(x) +When a type checker checks the implementation for consistency with overloads, +it should first apply any transforms that change the effective type of the +implementation including the presence of a ``yield`` statement in the +implementation body, the use of ``async def``, and the presence of additional +decorators. + + Overlapping overloads ^^^^^^^^^^^^^^^^^^^^^ -If two overloads can accept the same set of arguments but have -different return types, the overloads are said to "partially overlap". -This condition is indicative of a programming error and should -be reported by type checkers:: +If two overloads can accept the same set of arguments, they are said +to "partially overlap". If two overloads partially overlap, the return type +of the latter overload should be assignable to the return type of the +former overload. If this condition doesn't hold, it is indicative of a +programming error and should be reported by type checkers:: # These overloads partially overlap because both accept an # argument of type ``Literal[0]``, but their return types # differ. @overload - def func(x: Literal[0]) -> int: ... + def func1(x: Literal[0]) -> int: ... @overload - def func(x: int) -> str: ... + def func1(x: int) -> str: ... + +[Eric's note for reviewers: Mypy exempts `__get__` from the above check. +Refer to https://github.com/python/typing/issues/253#issuecomment-389262904 +for Ivan's explanation. I'm not convinced this exemption is necessary. +Currently pyright copies the exemption. Do we want to codify this or leave it +out?] If all arguments accepted by an overload are also always accepted by an earlier overload, the two overloads are said to "fully overlap". @@ -218,7 +237,7 @@ input signatures. - If no candidate overloads remain, generate an error and stop. - If only one candidate overload remains, it is the winning match. Evaluate -it as if it were a non-overloaded function call and stop. + it as if it were a non-overloaded function call and stop. - If two or more candidate overloads remain, proceed to step 2. @@ -230,7 +249,7 @@ Simply record which of the overloads result in evaluation errors. - If all overloads result in errors, proceed to step 3. - If only one overload evaluates without error, it is the winning match. -Evaluate it as if it were a non-overloaded function call and stop. + Evaluate it as if it were a non-overloaded function call and stop. - If two or more candidate overloads remain, proceed to step 4. @@ -253,11 +272,12 @@ After each argument expansion, return to step 2 and evaluate all expanded argument lists. - If all argument lists evaluate successfully, combine their -respective return types by union to determine the final return type -for the call, and stop. + respective return types by union to determine the final return type + for the call, and stop. - If argument expansion has been applied to all arguments and one or -more of the expanded argument lists cannot be evaluated successfully, -generate an error and stop. + more of the expanded argument lists cannot be evaluated successfully, + generate an error and stop. + For additional details about argument type expansion, see :ref:`argument-type-expansion` below. @@ -270,29 +290,30 @@ that supplies an indeterminate number of positional or keyword arguments. If so, eliminate overloads that do not have a variadic parameter. - If this results in only one remaining candidate overload, it is -the winning match. Evaluate it as if it were a non-overloaded function -call and stop. + the winning match. Evaluate it as if it were a non-overloaded function + call and stop. - If two or more candidate overloads remain, proceed to step 5. Step 5: If the type of one or more arguments evaluates to a -:ref:`gradual type` (e.g. ``list[Any]`` or ``str | Any``), determine -whether some theoretical :ref:`materialization` of these gradual types -could be used to disambiguate between two or more of the remaining -overloads. +type that includes a :term:`gradual form` (e.g. ``list[Any]`` or +``str | Any``), determine whether some theoretical +:ref:`materialization` of these gradual types could be used to disambiguate +between two or more of the remaining overloads. - If none of the arguments evaluate to a gradual type, proceed to step 6. - If one or more arguments evaluate to a gradual type but no possible -materializations of these types would disambiguate between the remaining -overloads, proceed to step 6. + materializations of these types would disambiguate between the remaining + overloads, proceed to step 6. - If possible materializations of these types would disambiguate between -two or more of the remaining overloads and this subset of overloads have -consistent return types, proceed to step 6. If the return types include -type variables, constraint solving should be applied here before testing -for consistency. + two or more of the remaining overloads and this subset of overloads have + consistent return types, proceed to step 6. If the return types include + type variables, constraint solving should be applied here before testing + for consistency. - If none of the above conditions are met, the presence of gradual types -leads to an ambiguous overload selection. Assume a return type of ``Any`` -and stop. This preserves the "gradual guarantee". + leads to an ambiguous overload selection. Assume a return type of ``Any`` + and stop. This preserves the "gradual guarantee". + [Eric's note for reviewers: I'm struggling to come up with an understandable and unambiguous way to describe this step. @@ -386,27 +407,19 @@ Example 4:: def example4(x: int, y: int) -> list[int]: ... def test(v1: list[Any], v2: Any): - # Step 4 eliminates third overload. Step 5 - # determines that first and second overloads - # both apply and are ambiguous due to Any, but - # return types are consistent. - r1 = example4(v1, 1) - reveal_type(r1) # Should reveal int - - # Step 4 eliminates third overload. Step 5 + # Step 2 eliminates the third overload. Step 5 # determines that first and second overloads # both apply and are ambiguous due to Any, but # return types are consistent. - r2 = example4([1], v2) - reveal_type(r2) # Should reveal int - - # Step 5 determines that first, second, and third - # overloads all apply and are ambiguous due to Any. - # These have inconsistent return types, so evaluated - # type is Any. - r3 = example4(v2, v2) - reveal_type(r3) # Should reveal Any + r1 = example4(v1, v2) + reveal_type(r1) # Reveals int + # Step 2 eliminates the second overload. Step 5 + # determines that first and third overloads + # both apply and are ambiguous due to Any, and + # the return types are inconsistent. + r2 = example4(v2, 1) + reveal_type(r2) # Should reveal Any Argument type expansion @@ -482,3 +495,10 @@ complexity to call evaluations and would likely result in a measurable slowdown in type evaluation, but it's worth considering. We could perhaps mitigate the slowdown by applying this behavior only when a constrained type variable is used in the call's signature.] + +[Eric's note for reviewers: What about expansion based on multiple inheritance? +For example, if class C inherits from A and B, should we expand C into A and B +for purposes of overload matching? This could get very expensive and difficult +to spec, and it feels like a significant edge case, so I'm inclined to leave it +out. No one has asked for this, to my knowledge.] + From f993b28eaa194c7611ec380ff8a324596ff3279b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 16 Aug 2024 18:57:17 +0000 Subject: [PATCH 04/14] [pre-commit.ci] auto fixes from pre-commit.com hooks --- docs/spec/overload.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/spec/overload.rst b/docs/spec/overload.rst index 5d2fca779..278e95763 100644 --- a/docs/spec/overload.rst +++ b/docs/spec/overload.rst @@ -196,7 +196,7 @@ programming error and should be reported by type checkers:: @overload def func1(x: int) -> str: ... -[Eric's note for reviewers: Mypy exempts `__get__` from the above check. +[Eric's note for reviewers: Mypy exempts `__get__` from the above check. Refer to https://github.com/python/typing/issues/253#issuecomment-389262904 for Ivan's explanation. I'm not convinced this exemption is necessary. Currently pyright copies the exemption. Do we want to codify this or leave it @@ -501,4 +501,3 @@ For example, if class C inherits from A and B, should we expand C into A and B for purposes of overload matching? This could get very expensive and difficult to spec, and it feels like a significant edge case, so I'm inclined to leave it out. No one has asked for this, to my knowledge.] - From 660295c78acb5624715531ddcf0553e048d80e94 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 16 Aug 2024 12:08:20 -0700 Subject: [PATCH 05/14] Fixed reference. --- docs/spec/overload.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/spec/overload.rst b/docs/spec/overload.rst index 5d2fca779..b6a13735e 100644 --- a/docs/spec/overload.rst +++ b/docs/spec/overload.rst @@ -280,7 +280,7 @@ expanded argument lists. For additional details about argument type expansion, see -:ref:`argument-type-expansion` below. +`argument-type-expansion`_ below. Step 4: If the argument list is compatible with two or more overloads, @@ -422,6 +422,8 @@ Example 4:: reveal_type(r2) # Should reveal Any +.. _argument-type-expansion: + Argument type expansion ^^^^^^^^^^^^^^^^^^^^^^^ From 831945b80004920dbd7ec64f0ba6f846df70ac50 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 16 Aug 2024 12:12:44 -0700 Subject: [PATCH 06/14] Fixed reference. --- docs/spec/overload.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/spec/overload.rst b/docs/spec/overload.rst index af81c02e0..83bfb580a 100644 --- a/docs/spec/overload.rst +++ b/docs/spec/overload.rst @@ -298,7 +298,7 @@ If so, eliminate overloads that do not have a variadic parameter. Step 5: If the type of one or more arguments evaluates to a type that includes a :term:`gradual form` (e.g. ``list[Any]`` or ``str | Any``), determine whether some theoretical -:ref:`materialization` of these gradual types could be used to disambiguate +:term:`materialization` of these gradual types could be used to disambiguate between two or more of the remaining overloads. - If none of the arguments evaluate to a gradual type, proceed to step 6. From 3906a1263ef5bd0cc6a8b5855ca8c5ca7d842444 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 16 Aug 2024 12:16:17 -0700 Subject: [PATCH 07/14] Another reference fix. --- docs/spec/overload.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/spec/overload.rst b/docs/spec/overload.rst index 83bfb580a..2eebcddee 100644 --- a/docs/spec/overload.rst +++ b/docs/spec/overload.rst @@ -298,8 +298,8 @@ If so, eliminate overloads that do not have a variadic parameter. Step 5: If the type of one or more arguments evaluates to a type that includes a :term:`gradual form` (e.g. ``list[Any]`` or ``str | Any``), determine whether some theoretical -:term:`materialization` of these gradual types could be used to disambiguate -between two or more of the remaining overloads. +:term:`materialization ` of these gradual types could be used +to disambiguate between two or more of the remaining overloads. - If none of the arguments evaluate to a gradual type, proceed to step 6. - If one or more arguments evaluate to a gradual type but no possible From bb8fe09e033b1e7931ad5b392291984279c9847e Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 22 Aug 2024 18:24:22 -0700 Subject: [PATCH 08/14] Incorporated PR feedback. --- docs/spec/overload.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/spec/overload.rst b/docs/spec/overload.rst index 2eebcddee..aecf84702 100644 --- a/docs/spec/overload.rst +++ b/docs/spec/overload.rst @@ -377,7 +377,7 @@ Example 3:: @overload def example3(*args: int) -> tuple[int, ...]: ... - def test(): + def test(val: list[int]): # Step 1 eliminates second overload. Step 4 and # step 5 do not apply. Step 6 picks the first # overload. @@ -392,7 +392,6 @@ Example 3:: # Step 1 doesn't eliminate any overloads. Step 4 # picks the third overload. - val = [1, 2, 3, 4] r3 = example3(*val) reveal_type(r3) # Should reveal tuple[int, ...] From cce3879e735f401fdd4fe0654913d0d23c351076 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 27 Aug 2024 17:06:38 -0700 Subject: [PATCH 09/14] Made changes to proposed overload chapter based on reviewer feedback. --- docs/spec/overload.rst | 194 ++++++++++++++--------------------------- 1 file changed, 67 insertions(+), 127 deletions(-) diff --git a/docs/spec/overload.rst b/docs/spec/overload.rst index aecf84702..09d91fefd 100644 --- a/docs/spec/overload.rst +++ b/docs/spec/overload.rst @@ -139,8 +139,9 @@ If one or more overloads are decorated with ``@final`` or ``@override`` but the implementation is not, an error should be reported. Overloads are allowed to use a mixture of ``async def`` and ``def`` statements -within the same overload definition. Type checkers should desugar all -``async def`` statements before testing for implementation consistency +within the same overload definition. Type checkers should convert +``async def`` statements to a non-async signature (wrapping the return +type in a ``Coroutine``) before testing for implementation consistency and overlapping overloads (described below). @@ -152,9 +153,9 @@ that it is consistent with all of its associated overload signatures. The implementation should accept all potential sets of arguments that are accepted by the overloads and should produce all potential return types produced by the overloads. In typing terms, this means the input -signature of the implementation should be assignable to the input signatures -of all overloads, and the return type of all overloads should be assignable to -the return type of the implementation. +signature of the implementation should be :term: to the input +signatures of all overloads, and the return type of all overloads should be +assignable to the return type of the implementation. If the implementation is inconsistent with its overloads, a type checker should report an error:: @@ -183,33 +184,32 @@ Overlapping overloads If two overloads can accept the same set of arguments, they are said to "partially overlap". If two overloads partially overlap, the return type -of the latter overload should be assignable to the return type of the -former overload. If this condition doesn't hold, it is indicative of a +of the former overload should be assignable to the return type of the +latter overload. If this condition doesn't hold, it is indicative of a programming error and should be reported by type checkers:: # These overloads partially overlap because both accept an - # argument of type ``Literal[0]``, but their return types - # differ. + # argument of type Literal[0], but the return type int is + # not assignable to str. @overload def func1(x: Literal[0]) -> int: ... @overload def func1(x: int) -> str: ... -[Eric's note for reviewers: Mypy exempts `__get__` from the above check. -Refer to https://github.com/python/typing/issues/253#issuecomment-389262904 -for Ivan's explanation. I'm not convinced this exemption is necessary. -Currently pyright copies the exemption. Do we want to codify this or leave it -out?] +Type checkers may exempt certain magic methods from the above check +for conditions that are mandated by their usage in the runtime. For example, +the ``__get__`` method of a descriptor is often defined using overloads +that would partially overlap if the above rule is enforced. -If all arguments accepted by an overload are also always accepted by -an earlier overload, the two overloads are said to "fully overlap". +If all possible sets of arguments accepted by an overload are also always +accepted by an earlier overload, the two overloads are said to "fully overlap". In this case, the latter overload will never be used. This condition is indicative of a programming error and should be reported by type checkers:: - # These overloads fully overlap because the second overload - # accepts all arguments accepted by the first overload. + # These overloads fully overlap because the first overload + # accepts all arguments accepted by the second overload. @overload def func[T](x: T) -> T: ... @@ -217,6 +217,14 @@ checkers:: def func(x: int) -> int: ... +[Eric's note for reviewers: We've identified a number of subtle issues and +cases where current type checkers do not honor the above rules, especially +for partially-overlapping overloads. At this point, I'm tempted to delete +this section entirely. We could always add it back to the spec later +if and when we find that there's a need for it and we achieve consensus on +the details.] + + Overload call evaluation ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -254,21 +262,20 @@ Simply record which of the overloads result in evaluation errors. Step 3: If step 2 produces errors for all overloads, perform -"argument type expansion". Some types can be decomposed -into two or more subtypes. For example, the type ``int | str`` can be -expanded into ``int`` and ``str``. +"argument type expansion". Union types can be expanded +into their constituent subtypes. For example, the type ``int | str`` can +be expanded into ``int`` and ``str``. -Expansion should be performed one argument at a time from left to +Type expansion should be performed one argument at a time from left to right. Each expansion results in sets of effective argument types. For example, if there are two arguments whose types evaluate to ``int | str`` and ``int | bytes``, expanding the first argument type -results in two sets of argument types: ``(int, ?)`` and ``(str, ?)``. -Here ``?`` represents an unexpanded argument type. -If expansion of the second argument is required, four sets of -argument types are produced: ``(int, int)``, ``(int, bytes)``, +results in two sets of argument types: ``(int, int | bytes)`` and +``(str, int | bytes)``. If type expansion for the second argument is required, +four sets of argument types are produced: ``(int, int)``, ``(int, bytes)``, ``(str, int)``, and ``(str, bytes)``. -After each argument expansion, return to step 2 and evaluate all +After each argument's expansion, return to step 2 and evaluate all expanded argument lists. - If all argument lists evaluate successfully, combine their @@ -295,45 +302,31 @@ If so, eliminate overloads that do not have a variadic parameter. - If two or more candidate overloads remain, proceed to step 5. -Step 5: If the type of one or more arguments evaluates to a -type that includes a :term:`gradual form` (e.g. ``list[Any]`` or -``str | Any``), determine whether some theoretical -:term:`materialization ` of these gradual types could be used -to disambiguate between two or more of the remaining overloads. - -- If none of the arguments evaluate to a gradual type, proceed to step 6. -- If one or more arguments evaluate to a gradual type but no possible - materializations of these types would disambiguate between the remaining - overloads, proceed to step 6. -- If possible materializations of these types would disambiguate between - two or more of the remaining overloads and this subset of overloads have - consistent return types, proceed to step 6. If the return types include - type variables, constraint solving should be applied here before testing - for consistency. -- If none of the above conditions are met, the presence of gradual types - leads to an ambiguous overload selection. Assume a return type of ``Any`` - and stop. This preserves the "gradual guarantee". - - -[Eric's note for reviewers: I'm struggling to come up with an -understandable and unambiguous way to describe this step. -Suggestions are welcome.] - -[Eric's note for reviewers: Pyright currently does not use return type -consistency in the above check. Instead, it looks for non-overlapping -return types. If return types are overlapping (that is, one is a consistent -subtype of another), it uses the wider return type. Only if there is no -consistency relationship between return types does it consider it an -"ambiguous" situation and turns it into an Any. This produces better -results for users of language servers, but it doesn't strictly preserve -the gradual guarantee. I'm willing to abandon this in favor of a -strict consistency check.] +Step 5: For each argument, determine whether all possible +:term:`materializations ` of the argument's type are assignable to +the corresponding parameter type for each of the remaining overloads. If so, +eliminate all of the subsequent remaining overloads. + +For example, if the argument type is ``list[Any]`` and there are three remaining +overloads with corresponding parameter types of ``list[int]``, ``list[Any]`` +and ``Any``. We can eliminate the third of the remaining overloads because +all manifestations of ``list[Any]`` are assignable to ``list[Any]``, the parameter +in the second overload. We cannot eliminate the second overload because there +are possible manifestations of ``list[Any]`` (for example, ``list[str]``) that +are not assignable to ``list[int]``. + +Once this filtering process is applied for all arguments, examine the return +types of the remaining overloads. If these return types include type variables, +they should be replaced with their solved types. If the resulting return types +for all remaining overloads are :term:, proceed to step 6. + +If the return types are not equivalent, overload matching is ambiguous. In +this case, assume a return type of ``Any`` and stop. Step 6: Choose the first remaining candidate overload as the winning match. Evaluate it as if it were a non-overloaded function call and stop. - Example 1:: @overload @@ -356,24 +349,24 @@ Example 2:: @overload def example2(x: int, y: int, z: int) -> int: ... - def test(val: str | int): + def test(values: list[str | int]): # In this example, argument type expansion is # performed on the first two arguments. Expansion # of the third is unnecessary. - r1 = example2(1, val, 1) + r1 = example2(1, values[0], 1) reveal_type(r1) # Should reveal str | int # Here, the types of all three arguments are expanded # without success. - example2(val, val, val) # Error in step 3 + example2(values[0], values[1], values[2]) # Error in step 3 Example 3:: @overload - def example3(x: int) -> int: ... + def example3(x: int, /) -> tuple[int]: ... @overload - def example3(x: int, y: int) -> tuple[int, int]: ... + def example3(x: int, y: int, /) -> tuple[int, int]: ... @overload def example3(*args: int) -> tuple[int, ...]: ... @@ -426,11 +419,12 @@ Example 4:: Argument type expansion ^^^^^^^^^^^^^^^^^^^^^^^ -When performing argument type expansion, the following types should be -expanded: +When performing argument type expansion, a type that is equivalent to +a union of a finite set of subtypes should be expanded into its constituent +subtypes. This includes the following cases. -1. Unions: Each subtype of the union should be considered as a separate -argument type. For example, the type ``int | str`` should be expanded +1. Explicit unions: Each subtype of the union should be considered as a +separate argument type. For example, the type ``int | str`` should be expanded into ``int`` and ``str``. 2. ``bool`` should be expanded into ``Literal[True]`` and ``Literal[False]``. @@ -441,64 +435,10 @@ be expanded into their literal members. 4. ``type[A | B]`` should be expanded into ``type[A]`` and ``type[B]``. 5. Tuples of known length that contain expandable types should be expanded -into all possible combinations of their subtypes. For example, the type +into all possible combinations of their element types. For example, the type ``tuple[int | str, bool]`` should be expanded into ``(int, Literal[True])``, ``(int, Literal[False])``, ``(str, Literal[True])``, and ``(str, Literal[False])``. - -[Eric's note for reviewers: I'm not 100% convinced we should -support argument expansion in all of these cases. Tuple expansion, -in particular, can be very costly and can quickly blow up in complexity. -Currently, pyright and mypy support only the case 1 in the list above, -but I have had requests to support 2 and 3.] - -When performing type expansion for an argument, the argument that -is targeted for expansion should be evaluated without the use of -any context. All arguments that are not yet expanded should -continue to be evaluated with the benefit of context supplied by parameter -types within each overload signature. - -Example:: - - class MyDict[T](TypedDict): - x: T - - @overload - def func[T](a: int, b: MyDict[T]) -> T: ... - - @overload - def func(a: str, b: dict[str, int]) -> str: ... - - - def test(val: int | str): - result = func(val, {'x': 1}) - reveal_type(result) # Should reveal "int | str" - -In this case, type expansion is performed on the first argument, -which expands its type from ``int | str`` to ``int`` and ``str``. -The expression for the second argument is evaluated in the context -of both overloads. For the first overload, the second argument evaluates -to ``MyDict[int]``, and for the second overload it evaluates to -``dict[str, int]``. Both overloads are used to evaluate this call, -and the final type of ``result`` is ``int | str``. - -[Eric's note: mypy apparently doesn't do this currently. It evaluates all -arguments without the benefit of context, which produces less-than-ideal -results in some cases.] - - -[Eric's note for reviewers: We may want to provide for argument type expansion -for regular (non-overloaded) calls as well. This came up recently in -[this thread](https://discuss.python.org/t/proposal-relax-un-correlated-constrained-typevars/59658). -I'm a bit hesitant to add this to the spec because it adds significant -complexity to call evaluations and would likely result in a measurable slowdown -in type evaluation, but it's worth considering. We could perhaps mitigate the -slowdown by applying this behavior only when a constrained type variable is -used in the call's signature.] - -[Eric's note for reviewers: What about expansion based on multiple inheritance? -For example, if class C inherits from A and B, should we expand C into A and B -for purposes of overload matching? This could get very expensive and difficult -to spec, and it feels like a significant edge case, so I'm inclined to leave it -out. No one has asked for this, to my knowledge.] +The above list may not be exhaustive, and additional cases may be added in +the future as the type system evolves. From 7591a4dda7d1f3668b4e8233efffe5575cc91aef Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 28 Aug 2024 10:03:26 -0700 Subject: [PATCH 10/14] Incorporated additional feedback from reviewers. --- docs/spec/overload.rst | 54 ++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/docs/spec/overload.rst b/docs/spec/overload.rst index 09d91fefd..5a70d2985 100644 --- a/docs/spec/overload.rst +++ b/docs/spec/overload.rst @@ -130,13 +130,19 @@ checkers should report an error or warning if an implementation is missing. Overload definitions within stub files, protocols, and abstract base classes are exempt from this check. -If an overload is decorated with ``@staticmethod`` or ``@classmethod``, -all overloads must be similarly decorated. The implementation, -if present, must be decorated in the same manner. Type checkers should report -an error if these conditions are not met. - -If one or more overloads are decorated with ``@final`` or ``@override`` but the -implementation is not, an error should be reported. +If one overload signature is decorated with ``@staticmethod`` or +``@classmethod``, all overload signatures must be similarly decorated. The +implementation, if present, must also have a consistent decorator. Type +checkers should report an error if these conditions are not met. + +If a ``@final`` or ``@override`` decorator is supplied for a function with +overloads, the decorator should be applied only to the overload implementation +if it is present. If an overload implementation isn't present (for example, in +a stub file), the ``@final`` or ``@override`` decorator should be applied only +to the first overload. Type checkers should enforce these rules and generate +an error when they are violated. If a ``@final`` or ``@override`` decorator +follows these rules, a type checker should treat the decorator as if it is +present on all overloads. Overloads are allowed to use a mixture of ``async def`` and ``def`` statements within the same overload definition. Type checkers should convert @@ -202,6 +208,21 @@ for conditions that are mandated by their usage in the runtime. For example, the ``__get__`` method of a descriptor is often defined using overloads that would partially overlap if the above rule is enforced. +Type checkers may ignore the possibility of multiple inheritance or +intersections involving structural types for purposes of computing overlap. +In the following example, classes ``A`` and ``B`` could theoretically overlap +because there could be a common type ``C`` that derives from both ``A`` and +``B``, but type checkers may choose not to flag this as an overlapping +overload:: + + class A: ... + class B: ... + + @overload + def func(x: A) -> int: ... + @overload + def func(x: B) -> str: ... + If all possible sets of arguments accepted by an overload are also always accepted by an earlier overload, the two overloads are said to "fully overlap". In this case, the latter overload will never be used. This condition @@ -217,21 +238,14 @@ checkers:: def func(x: int) -> int: ... -[Eric's note for reviewers: We've identified a number of subtle issues and -cases where current type checkers do not honor the above rules, especially -for partially-overlapping overloads. At this point, I'm tempted to delete -this section entirely. We could always add it back to the spec later -if and when we find that there's a need for it and we achieve consensus on -the details.] - - Overload call evaluation ^^^^^^^^^^^^^^^^^^^^^^^^ When a type checker evaluates the call of an overloaded function, it attempts to "match" the supplied arguments with one or more overloads. This section describes the algorithm that type checkers should use -for overload matching. +for overload matching. This algorithm should be applied even in the +presence of :ref:. Only the overloads (the ``@overload``-decorated signatures) should be considered for matching purposes. The implementation, if provided, @@ -310,10 +324,10 @@ eliminate all of the subsequent remaining overloads. For example, if the argument type is ``list[Any]`` and there are three remaining overloads with corresponding parameter types of ``list[int]``, ``list[Any]`` and ``Any``. We can eliminate the third of the remaining overloads because -all manifestations of ``list[Any]`` are assignable to ``list[Any]``, the parameter -in the second overload. We cannot eliminate the second overload because there -are possible manifestations of ``list[Any]`` (for example, ``list[str]``) that -are not assignable to ``list[int]``. +all materializations of ``list[Any]`` are assignable to ``list[Any]``, the +parameter in the second overload. We cannot eliminate the second overload +because there are possible materializations of ``list[Any]`` (for example, +``list[str]``) that are not assignable to ``list[int]``. Once this filtering process is applied for all arguments, examine the return types of the remaining overloads. If these return types include type variables, From 91d4adce96eee9bb4b9266a2ca78bcb9a3ee5080 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 28 Aug 2024 19:42:53 -0700 Subject: [PATCH 11/14] Incorporated more feedback. --- docs/spec/overload.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/spec/overload.rst b/docs/spec/overload.rst index 5a70d2985..6e574dcaa 100644 --- a/docs/spec/overload.rst +++ b/docs/spec/overload.rst @@ -192,16 +192,15 @@ If two overloads can accept the same set of arguments, they are said to "partially overlap". If two overloads partially overlap, the return type of the former overload should be assignable to the return type of the latter overload. If this condition doesn't hold, it is indicative of a -programming error and should be reported by type checkers:: - - # These overloads partially overlap because both accept an - # argument of type Literal[0], but the return type int is - # not assignable to str. +programming error and should be reported by type checkers. The purpose of +this check is to prevent unsoundness of this form:: @overload - def func1(x: Literal[0]) -> int: ... + def is_one(x: Literal[0]) -> Literal[True]: ... @overload - def func1(x: int) -> str: ... + def is_one(x: int) -> Literal[False]: ... + + reveal_type(is_one(int(1))) # Reveals Literal[False], but True at runtime Type checkers may exempt certain magic methods from the above check for conditions that are mandated by their usage in the runtime. For example, From 69d6d4a453f6f72f10342211edce586bc2ae5cad Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 13 Dec 2024 13:18:09 -0800 Subject: [PATCH 12/14] Fixed typo in code sample. --- docs/spec/overload.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/spec/overload.rst b/docs/spec/overload.rst index 6e574dcaa..074d540a1 100644 --- a/docs/spec/overload.rst +++ b/docs/spec/overload.rst @@ -196,7 +196,7 @@ programming error and should be reported by type checkers. The purpose of this check is to prevent unsoundness of this form:: @overload - def is_one(x: Literal[0]) -> Literal[True]: ... + def is_one(x: Literal[1]) -> Literal[True]: ... @overload def is_one(x: int) -> Literal[False]: ... From e13dbbeb4b9436fae86b33ca292475cb02191c39 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 16 Dec 2024 12:25:36 -0800 Subject: [PATCH 13/14] Update docs/spec/overload.rst Co-authored-by: Eneg <42005170+Enegg@users.noreply.github.com> --- docs/spec/overload.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/spec/overload.rst b/docs/spec/overload.rst index 074d540a1..78c4e5017 100644 --- a/docs/spec/overload.rst +++ b/docs/spec/overload.rst @@ -388,7 +388,7 @@ Example 3:: # step 5 do not apply. Step 6 picks the first # overload. r1 = example3(1) - reveal_type(r1) # Should reveal int + reveal_type(r1) # Should reveal tuple[int] # Step 1 eliminates first overload. Step 4 and # step 5 do not apply. Step 6 picks the second From 57495db72d7ba81ae952b3b601ca3a52a5f26935 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 8 Jan 2025 17:48:27 -0800 Subject: [PATCH 14/14] (very) initial steps on conformance tests --- conformance/results/mypy/overloads_basic.toml | 25 ++++-- conformance/results/mypy/version.toml | 4 +- conformance/results/pyre/overloads_basic.toml | 21 +++-- conformance/results/pyre/version.toml | 2 +- .../results/pyright/overloads_basic.toml | 32 +++++-- conformance/results/pyright/version.toml | 2 +- .../results/pytype/overloads_basic.toml | 43 ++++++++-- conformance/results/pytype/version.toml | 2 +- conformance/results/results.html | 18 ++-- conformance/tests/overloads_basic.py | 83 ++++++++++++++++--- 10 files changed, 180 insertions(+), 52 deletions(-) diff --git a/conformance/results/mypy/overloads_basic.toml b/conformance/results/mypy/overloads_basic.toml index c42562c94..81cda1334 100644 --- a/conformance/results/mypy/overloads_basic.toml +++ b/conformance/results/mypy/overloads_basic.toml @@ -1,12 +1,21 @@ -conformant = "Pass" +conformant = "Partial" +notes = """ +Does not allow an overload with no implementation in an abstract base class. +""" output = """ -overloads_basic.py:37: error: No overload variant of "__getitem__" of "Bytes" matches argument type "str" [call-overload] -overloads_basic.py:37: note: Possible overload variants: -overloads_basic.py:37: note: def __getitem__(self, int, /) -> int -overloads_basic.py:37: note: def __getitem__(self, slice[Any, Any, Any], /) -> bytes -overloads_basic.py:62: error: Single overload definition, multiple required [misc] -overloads_basic.py:74: error: An overloaded function outside a stub file must have an implementation [no-overload-impl] +overloads_basic.py:41: error: No overload variant of "__getitem__" of "Bytes" matches argument type "str" [call-overload] +overloads_basic.py:41: note: Possible overload variants: +overloads_basic.py:41: note: def __getitem__(self, int, /) -> int +overloads_basic.py:41: note: def __getitem__(self, slice[Any, Any, Any], /) -> bytes +overloads_basic.py:66: error: Single overload definition, multiple required [misc] +overloads_basic.py:78: error: An overloaded function outside a stub file must have an implementation [no-overload-impl] +overloads_basic.py:101: error: An overloaded function outside a stub file must have an implementation [no-overload-impl] +overloads_basic.py:116: error: Overload does not consistently use the "@staticmethod" decorator on all function signatures. [misc] +overloads_basic.py:126: error: Overloaded function implementation does not accept all possible arguments of signature 1 [misc] +overloads_basic.py:126: error: Overloaded function implementation does not accept all possible arguments of signature 2 [misc] +overloads_basic.py:129: error: Overload does not consistently use the "@classmethod" decorator on all function signatures. [misc] """ -conformance_automated = "Pass" +conformance_automated = "Fail" errors_diff = """ +Line 101: Unexpected errors ['overloads_basic.py:101: error: An overloaded function outside a stub file must have an implementation [no-overload-impl]'] """ diff --git a/conformance/results/mypy/version.toml b/conformance/results/mypy/version.toml index 3d498066d..1012f096b 100644 --- a/conformance/results/mypy/version.toml +++ b/conformance/results/mypy/version.toml @@ -1,2 +1,2 @@ -version = "mypy 1.14.0" -test_duration = 1.6 +version = "mypy 1.14.1" +test_duration = 1.7 diff --git a/conformance/results/pyre/overloads_basic.toml b/conformance/results/pyre/overloads_basic.toml index 7a5d5b54d..bd7aa8fd3 100644 --- a/conformance/results/pyre/overloads_basic.toml +++ b/conformance/results/pyre/overloads_basic.toml @@ -1,11 +1,22 @@ -conformant = "Pass" +conformant = "Partial" notes = """ +Does not allow an overload with no implementation in a Protocol or an abstract base class. """ output = """ -overloads_basic.py:37:2 Incompatible parameter type [6]: In call `Bytes.__getitem__`, for 1st positional argument, expected `int` but got `str`. -overloads_basic.py:63:0 Incompatible overload [43]: At least two overload signatures must be present. -overloads_basic.py:75:0 Missing overload implementation [42]: Overloaded function `func2` must have an implementation. +overloads_basic.py:41:2 Incompatible parameter type [6]: In call `Bytes.__getitem__`, for 1st positional argument, expected `int` but got `str`. +overloads_basic.py:67:0 Incompatible overload [43]: At least two overload signatures must be present. +overloads_basic.py:79:0 Missing overload implementation [42]: Overloaded function `func2` must have an implementation. +overloads_basic.py:92:4 Missing overload implementation [42]: Overloaded function `MyProto.func3` must have an implementation. +overloads_basic.py:102:4 Missing overload implementation [42]: Overloaded function `MyAbstractBase.func4` must have an implementation. +overloads_basic.py:118:4 Incompatible overload [43]: The implementation of `C.func5` does not accept all possible arguments of overload defined on line `118`. +overloads_basic.py:123:4 Incompatible overload [43]: The implementation of `C.func5` does not accept all possible arguments of overload defined on line `123`. +overloads_basic.py:126:4 Incompatible overload [43]: This definition does not have the same decorators as the preceding overload(s). +overloads_basic.py:131:4 Incompatible overload [43]: The implementation of `C.func6` does not accept all possible arguments of overload defined on line `131`. +overloads_basic.py:136:4 Incompatible overload [43]: The implementation of `C.func6` does not accept all possible arguments of overload defined on line `136`. +overloads_basic.py:139:4 Incompatible overload [43]: This definition does not have the same decorators as the preceding overload(s). """ -conformance_automated = "Pass" +conformance_automated = "Fail" errors_diff = """ +Line 92: Unexpected errors ['overloads_basic.py:92:4 Missing overload implementation [42]: Overloaded function `MyProto.func3` must have an implementation.'] +Line 102: Unexpected errors ['overloads_basic.py:102:4 Missing overload implementation [42]: Overloaded function `MyAbstractBase.func4` must have an implementation.'] """ diff --git a/conformance/results/pyre/version.toml b/conformance/results/pyre/version.toml index 94de30557..e22b34fde 100644 --- a/conformance/results/pyre/version.toml +++ b/conformance/results/pyre/version.toml @@ -1,2 +1,2 @@ version = "pyre 0.9.23" -test_duration = 7.3 +test_duration = 5.9 diff --git a/conformance/results/pyright/overloads_basic.toml b/conformance/results/pyright/overloads_basic.toml index 5171f52e2..b425209bb 100644 --- a/conformance/results/pyright/overloads_basic.toml +++ b/conformance/results/pyright/overloads_basic.toml @@ -1,11 +1,31 @@ -conformant = "Pass" +conformant = "Partial" +notes = """ +Does not allow an overload with no implementation in an abstract base class. +""" output = """ -overloads_basic.py:37:1 - error: No overloads for "__getitem__" match the provided arguments (reportCallIssue) -overloads_basic.py:37:1 - error: Argument of type "Literal['']" cannot be assigned to parameter "__s" of type "slice[Any, Any, Any]" in function "__getitem__" +overloads_basic.py:41:1 - error: No overloads for "__getitem__" match the provided arguments (reportCallIssue) +overloads_basic.py:41:1 - error: Argument of type "Literal['']" cannot be assigned to parameter "__s" of type "slice[Any, Any, Any]" in function "__getitem__"   "Literal['']" is not assignable to "slice[Any, Any, Any]" (reportArgumentType) -overloads_basic.py:63:5 - error: "func1" is marked as overload, but additional overloads are missing (reportInconsistentOverload) -overloads_basic.py:75:5 - error: "func2" is marked as overload, but no implementation is provided (reportNoOverloadImplementation) +overloads_basic.py:67:5 - error: "func1" is marked as overload, but additional overloads are missing (reportInconsistentOverload) +overloads_basic.py:79:5 - error: "func2" is marked as overload, but no implementation is provided (reportNoOverloadImplementation) +overloads_basic.py:102:9 - error: "func4" is marked as overload, but no implementation is provided (reportNoOverloadImplementation) +overloads_basic.py:118:9 - error: Overloads for "func5" use @staticmethod inconsistently (reportInconsistentOverload) +overloads_basic.py:126:9 - error: Overloaded implementation is not consistent with signature of overload 1 +  Type "(self: Self@C, x: int | str) -> (int | str)" is not assignable to type "(x: int) -> int" +    Parameter name mismatch: "x" versus "self" +    Parameter 1: type "int" is incompatible with type "Self@C" +      Type "int" is not assignable to type "Self@C" +    Extra parameter "x" (reportInconsistentOverload) +overloads_basic.py:126:9 - error: Overloaded implementation is not consistent with signature of overload 2 +  Type "(self: Self@C, x: int | str) -> (int | str)" is not assignable to type "(x: str) -> str" +    Parameter name mismatch: "x" versus "self" +    Parameter 1: type "str" is incompatible with type "Self@C" +      Type "str" is not assignable to type "Self@C" +    Extra parameter "x" (reportInconsistentOverload) +overloads_basic.py:131:9 - error: Overloads for "func6" use @classmethod inconsistently (reportInconsistentOverload) +overloads_basic.py:139:15 - warning: Instance methods should take a "self" parameter (reportSelfClsParameterName) """ -conformance_automated = "Pass" +conformance_automated = "Fail" errors_diff = """ +Line 102: Unexpected errors ['overloads_basic.py:102:9 - error: "func4" is marked as overload, but no implementation is provided (reportNoOverloadImplementation)'] """ diff --git a/conformance/results/pyright/version.toml b/conformance/results/pyright/version.toml index e8e963b77..1ae5a35b8 100644 --- a/conformance/results/pyright/version.toml +++ b/conformance/results/pyright/version.toml @@ -1,2 +1,2 @@ version = "pyright 1.1.391" -test_duration = 1.2 +test_duration = 1.5 diff --git a/conformance/results/pytype/overloads_basic.toml b/conformance/results/pytype/overloads_basic.toml index 06e69b8cb..4bcdb001b 100644 --- a/conformance/results/pytype/overloads_basic.toml +++ b/conformance/results/pytype/overloads_basic.toml @@ -2,28 +2,57 @@ conformant = "Partial" notes = """ Does not reject a function with a single @overload signature. Does not reject a function with @overload signature but no implementation. +Does not allow an overload with no implementation in a Protocol or an abstract base class. +Does not exempt overloads from checking of return type in body, when also decorated with `@staticmethod`. +Does not error on overloads inconsistently decorated with `@staticmethod` or `@classmethod`. """ output = """ -overloads_basic.py:31:20: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in __getitem__: bad return type [bad-return-type] +overloads_basic.py:35:20: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in __getitem__: bad return type [bad-return-type] return b"" \u001b[1m\u001b[31m~~~\u001b[39m\u001b[0m -overloads_basic.py:37:1: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in : unsupported operand type(s) for item retrieval: Bytes and str [unsupported-operands] +overloads_basic.py:41:1: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in : unsupported operand type(s) for item retrieval: Bytes and str [unsupported-operands] b[""] # E: no matching overload \u001b[1m\u001b[31m~~~~~\u001b[39m\u001b[0m -overloads_basic.py:58:5: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in map: bad return type [bad-return-type] +overloads_basic.py:62:5: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in map: bad return type [bad-return-type] pass \u001b[1m\u001b[31m~~~~\u001b[39m\u001b[0m +overloads_basic.py:98:9: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in func3: @typing.overload-decorated 'MyProto.func3' object is not callable [not-callable] + + ... + \u001b[1m\u001b[31m~~~\u001b[39m\u001b[0m + +overloads_basic.py:108:9: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in func4: @typing.overload-decorated 'MyAbstractBase.func4' object is not callable [not-callable] + + ... + \u001b[1m\u001b[31m~~~\u001b[39m\u001b[0m + +overloads_basic.py:119:9: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in func5: bad return type [bad-return-type] + + ... + \u001b[1m\u001b[31m~~~\u001b[39m\u001b[0m + +overloads_basic.py:124:9: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in func5: bad return type [bad-return-type] + + ... + \u001b[1m\u001b[31m~~~\u001b[39m\u001b[0m + """ conformance_automated = "Fail" errors_diff = """ -Lines 62, 63: Expected error (tag 'func1') -Lines 74, 75: Expected error (tag 'func2') -Line 31: Unexpected errors ['overloads_basic.py:31:20: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in __getitem__: bad return type [bad-return-type]'] -Line 58: Unexpected errors ['overloads_basic.py:58:5: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in map: bad return type [bad-return-type]'] +Lines 66, 67: Expected error (tag 'func1') +Lines 78, 79: Expected error (tag 'func2') +Lines 116, 118, 123, 126: Expected error (tag 'func5') +Lines 129, 131, 136, 139: Expected error (tag 'func6') +Line 35: Unexpected errors ['overloads_basic.py:35:20: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in __getitem__: bad return type [bad-return-type]'] +Line 62: Unexpected errors ['overloads_basic.py:62:5: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in map: bad return type [bad-return-type]'] +Line 98: Unexpected errors ["overloads_basic.py:98:9: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in func3: @typing.overload-decorated 'MyProto.func3' object is not callable [not-callable]"] +Line 108: Unexpected errors ["overloads_basic.py:108:9: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in func4: @typing.overload-decorated 'MyAbstractBase.func4' object is not callable [not-callable]"] +Line 119: Unexpected errors ['overloads_basic.py:119:9: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in func5: bad return type [bad-return-type]'] +Line 124: Unexpected errors ['overloads_basic.py:124:9: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in func5: bad return type [bad-return-type]'] """ diff --git a/conformance/results/pytype/version.toml b/conformance/results/pytype/version.toml index 94cd5eee5..c33e38931 100644 --- a/conformance/results/pytype/version.toml +++ b/conformance/results/pytype/version.toml @@ -1,2 +1,2 @@ version = "pytype 2024.10.11" -test_duration = 37.2 +test_duration = 30.3 diff --git a/conformance/results/results.html b/conformance/results/results.html index e388ee383..de3b3ff05 100644 --- a/conformance/results/results.html +++ b/conformance/results/results.html @@ -158,17 +158,17 @@

Python Type System Conformance Test Results

- - - - - + + + +
 
mypy 1.14.0
-
1.6sec
+
mypy 1.14.1
+
1.7sec
pyright 1.1.391
-
1.2sec
+
1.5sec
pyre 0.9.23
-
7.3sec
+
5.9sec
pytype 2024.10.11
-
37.2sec
+
30.3sec
@@ -667,10 +667,10 @@

Python Type System Conformance Test Results

Overloads
     overloads_basicPassPassPass
Partial

Does not reject a function with a single @overload signature.

Does not reject a function with @overload signature but no implementation.

Partial

Does not allow an overload with no implementation in an abstract base class.

Partial

Does not allow an overload with no implementation in an abstract base class.

Partial

Does not allow an overload with no implementation in a Protocol or an abstract base class.

Partial

Does not reject a function with a single @overload signature.

Does not reject a function with @overload signature but no implementation.

Does not allow an overload with no implementation in a Protocol or an abstract base class.

Does not exempt overloads from checking of return type in body, when also decorated with `@staticmethod`.

Does not error on overloads inconsistently decorated with `@staticmethod`.

Exceptions diff --git a/conformance/tests/overloads_basic.py b/conformance/tests/overloads_basic.py index 86c81765d..c243883fc 100644 --- a/conformance/tests/overloads_basic.py +++ b/conformance/tests/overloads_basic.py @@ -1,16 +1,20 @@ """ -Tests the basic typing.overload behavior described in PEP 484. +Tests the behavior of typing.overload. """ # Specification: https://typing.readthedocs.io/en/latest/spec/overload.html#overload -# Note: The behavior of @overload is severely under-specified by PEP 484 leading -# to significant divergence in behavior across type checkers. This is something -# we will likely want to address in a future update to the typing spec. For now, -# this conformance test will cover only the most basic functionality described -# in PEP 484. - -from typing import Any, Callable, Iterable, Iterator, TypeVar, assert_type, overload +from abc import ABC +from typing import ( + Any, + Callable, + Iterable, + Iterator, + Protocol, + TypeVar, + assert_type, + overload, +) class Bytes: @@ -58,7 +62,7 @@ def map(func: Any, iter1: Any, iter2: Any = ...) -> Any: pass -# At least two overload signatures should be provided. +# > At least two @overload-decorated definitions must be present. @overload # E[func1] def func1() -> None: # E[func1]: At least two overloads must be present ... @@ -68,9 +72,9 @@ def func1() -> None: pass -# > In regular modules, a series of @overload-decorated definitions must be -# > followed by exactly one non-@overload-decorated definition (for the same -# > function/method). +# > The ``@overload``-decorated definitions must be followed by an overload +# > implementation, which does not include an ``@overload`` decorator. Type +# > checkers should report an error or warning if an implementation is missing. @overload # E[func2] def func2(x: int) -> int: # E[func2]: no implementation ... @@ -79,3 +83,58 @@ def func2(x: int) -> int: # E[func2]: no implementation @overload def func2(x: str) -> str: ... + + +# > Overload definitions within stub files, protocols, and abstract base classes +# > are exempt from this check. +class MyProto(Protocol): + @overload + def func3(self, x: int) -> int: + ... + + + @overload + def func3(self, x: str) -> str: + ... + +class MyAbstractBase(ABC): + @overload + def func4(self, x: int) -> int: + ... + + + @overload + def func4(self, x: str) -> str: + ... + + +# > If one overload signature is decorated with ``@staticmethod`` or +# > ``@classmethod``, all overload signatures must be similarly decorated. The +# > implementation, if present, must also have a consistent decorator. Type +# > checkers should report an error if these conditions are not met. +class C: + @overload # E[func5] + @staticmethod + def func5(x: int) -> int: # E[func5] + ... + + @overload + @staticmethod + def func5(x: str) -> str: # E[func5] + ... + + def func5(self, x: int | str) -> int | str: # E[func5] + return 1 + + @overload # E[func6] + @classmethod + def func6(cls, x: int) -> int: # E[func6] + ... + + @overload + @classmethod + def func6(cls, x: str) -> str: # E[func6] + ... + + def func6(cls, x: int | str) -> int | str: # E[func6] + return 1