Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to the Overloads chapter #1839

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
bab532e
First draft of an update to the Overloads chapter.
erictraut Aug 13, 2024
de81026
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 13, 2024
5ca254e
Updated draft based on initial round of feedback.
erictraut Aug 16, 2024
33c819f
Merge branch 'overloads' of https://github.com/erictraut/typing into …
erictraut Aug 16, 2024
f993b28
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 16, 2024
660295c
Fixed reference.
erictraut Aug 16, 2024
06c86f1
Merge branch 'overloads' of https://github.com/erictraut/typing into …
erictraut Aug 16, 2024
831945b
Fixed reference.
erictraut Aug 16, 2024
3906a12
Another reference fix.
erictraut Aug 16, 2024
bb8fe09
Incorporated PR feedback.
erictraut Aug 23, 2024
cce3879
Made changes to proposed overload chapter based on reviewer feedback.
erictraut Aug 28, 2024
7591a4d
Incorporated additional feedback from reviewers.
erictraut Aug 28, 2024
91d4adc
Incorporated more feedback.
erictraut Aug 29, 2024
69d6d4a
Fixed typo in code sample.
erictraut Dec 13, 2024
e13dbbe
Update docs/spec/overload.rst
erictraut Dec 16, 2024
eed0815
Merge branch 'main' into overloads
carljm Jan 8, 2025
57495db
(very) initial steps on conformance tests
carljm Jan 9, 2025
27f1c79
fix abstractmethod without implementation check
carljm Jan 10, 2025
535075f
split overloads_invalid.py from overloads_basic.py
carljm Jan 10, 2025
8875a4a
add test for final with overload
carljm Jan 10, 2025
cb04dd6
add tests for correct usage of override with an overload
carljm Jan 10, 2025
5eabe53
add test for wrong use of override with overload
carljm Jan 10, 2025
484b03c
rename overloads_invalid to overloads_definitions
carljm Jan 10, 2025
ac3b70e
add support for stub test files, add overloads_definitions_stub.pyi
carljm Jan 10, 2025
87377ed
add initial overloads_consistency tests
carljm Jan 10, 2025
f7bf384
add tests for mixed async-def
carljm Jan 11, 2025
4936ac1
add tests for signature-transforming decorators
carljm Jan 11, 2025
e0e0b8a
add test for partially overlapping overloads
carljm Jan 11, 2025
cc748d3
add test for fully overlapping overloads
carljm Jan 11, 2025
17d3e15
add tests for step-1 of overload evaluation
carljm Jan 11, 2025
02f0652
add tests for steps 2 and 3 of overload evaluation
carljm Jan 11, 2025
f5bee93
add tests for bool expansion (no checker does this)
carljm Jan 11, 2025
169fa58
add tests for enum and tuple expansion
carljm Jan 11, 2025
c041484
add test for type[A | B] expansion
carljm Jan 11, 2025
c10a72d
add test for step 4 in overload matching
carljm Jan 11, 2025
8a98eae
add test for steps 5/6 in overload matching
carljm Jan 11, 2025
5d22e8d
no expectation of return type if there are call errors
carljm Jan 13, 2025
98f36e8
improve variadic test to not use overlapping overloads
carljm Jan 13, 2025
67c1675
Merge branch 'main' into overloads
erictraut Jan 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 67 additions & 127 deletions docs/spec/overload.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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).


Expand All @@ -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:<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::
Expand Down Expand Up @@ -183,40 +184,47 @@ Overlapping overloads

If two overloads can accept the same set of arguments, they are said
erictraut marked this conversation as resolved.
Show resolved Hide resolved
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::
erictraut marked this conversation as resolved.
Show resolved Hide resolved

# 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.
erictraut marked this conversation as resolved.
Show resolved Hide resolved

@overload
def func1(x: Literal[0]) -> int: ...
@overload
def func1(x: int) -> str: ...

erictraut marked this conversation as resolved.
Show resolved Hide resolved
[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: ...
@overload
def func(x: int) -> int: ...

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do they also fully overlap if this second signature is changed to (x: int) -> bool, or (x: bool) -> int? And what'll happen if either int is replaced by Any?
And how about when this second overload returns NoReturn or Any?



[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
^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down Expand Up @@ -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
Expand All @@ -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 <materialize>` 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 <materialize>` 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph felt unclear. I had to read it multiple times to understand the intent. Maybe reword this, or include some motivation why we have this rule, so that this can be understood easily without peeking at the following paragraph?


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
erictraut marked this conversation as resolved.
Show resolved Hide resolved
in the second overload. We cannot eliminate the second overload because there
are possible manifestations of ``list[Any]`` (for example, ``list[str]``) that
erictraut marked this conversation as resolved.
Show resolved Hide resolved
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:<equivalent>, 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.
Copy link
Member

@carljm carljm Jan 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a correctness requirement for the return type here to be assumed to be Any? It seems to me that it would also be valid for a type-checker to use the union of all the ambiguous matching overloads. I would prefer for the specification not to prevent that option. (No type checker currently does this.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existing stubs (including typeshed, numpy, pandas, and others) assume that the result will be Any in this case, so I don't think this is something we can change at this point. An earlier version of pyright generated a union, and it resulted in many false positive errors and lots of unhappy users. I think it's important for the spec to specify Any here so stub authors can rely on the behavior.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, ok, that's useful info, thank you. If you happen to know of any old pyright issues where unhappy users surfaced these problems with the union behavior, I would be curious to take a look at some real world cases relying on this.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existing stubs (including typeshed, numpy, pandas, and others) assume that the result will be Any in this case, so I don't think this is something we can change at this point.

I maintain the stubs of NumPy and SciPy, and I know those stubs better than I care to admit, but this doesn't ring a bell for me. Do you have an example?
Because if so, then I'd be more than willing to change that. I'd like to avoid having to deal with Any if I can (and not only because set("Any") < set("Annoying")).



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
Expand All @@ -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, ...]: ...

Expand Down Expand Up @@ -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]``.
Expand All @@ -441,64 +435,10 @@ be expanded into their literal members.
4. ``type[A | B]`` should be expanded into ``type[A]`` and ``type[B]``.
erictraut marked this conversation as resolved.
Show resolved Hide resolved

5. Tuples of known length that contain expandable types should be expanded
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that argument type expansion has to be in the overload rules because type checkers are already doing it, and library and stub authors are relying on it. But tuple expansion isn't currently done, right? Do we have evidence that this is useful/frequently requested? It seems potentially pretty expensive.

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.
Comment on lines +456 to +457

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok that's good to hear, because it would be pretty cool if "linear" user-defined types like numpy.dtype[A | B] could also be made distributive.