Replies: 2 comments 4 replies
-
@srittau suggested a version of this on the mailing list: https://mail.python.org/archives/list/[email protected]/message/7W5RMJVBRN6NE6A3LUP5Y4BMOZKQRYWH/. It didn't really go anywhere, and I'm not sure a PEP to make this change would be accepted, as the use case might be too narrow to justify new syntax. Your workaround seems reasonable though, that's something we can add to typing. I think it should still need a PEP. |
Beta Was this translation helpful? Give feedback.
-
My alternate proposal for this kind of situation that leads to messy overload explosion is have type level map that is only sugar for overload. Using your example, @overload
def foo(x: int, y: None = None, z: None = None) -> int: ...
@overload
def foo(x: int, y: None = None, z: Callable[[int], _T]) -> _T: ... can be written like, FooType = TypeMapping({None: int, Callable[[int], _T]: _T})
T = TypeVar('T')
def foo(x: int, y: None = None, z: T = None) -> FooType[T]: ... If you want to add a 3rd overload just add it to map as a key. TypeMapping here takes tuple[type, ...] -> type. Number of elements in tuple is fixed for a given type map but just like you can have one length tuple you could also have something like, FooType = TypeMapping({(int, str): int, (list[int], list[str]): list[int]})
T1 = TypeVar('T1')
T2 = TypeVar('T2')
def foo(x: T1, y: T2) -> FooType[T1, T2] would mean exact same thing as, @overload
def foo(x: int, y: str) -> int:
...
@overload
def foo(x: list[int], y: list[str]) -> list[int]:
... And FooType is reusable across multiple functions if you have several functions with similar overload patterns. There is some ambiguity as to default value although default values can only align with keys in map that match default value's type which should usually narrow it down. |
Beta Was this translation helpful? Give feedback.
-
I've come across this many times where a function has maybe four or five optional arguments that can either be positional or keyword arguments and the return type depends on one or two of the optional arguments in arbitrary positions. Now you're often forced to write at least two versions of every call where the optional argument is specified, to both match the positional and keyword case, since required positional arguments can't follow optional positional arguments.
But for overload matching this requirement shouldn't really matter, the rules for positional arguments still apply, so the type checker knows all the arguments preceding the required argument have to be set as well, if it's called positionally, but may be omitted if the argument is instead specified as a keyword argument, so it can generate the two possible overloads from a single overload itself.
So if we could violate the positional argument rules for specifying overloads we could write the following
instead of having to write
If you define further overloads for
y
that affect the result of the function you can see how this very quickly can get out of hand and you end up with many more overloads than you would like. The obvious solution would be to change the order of arguments or to make them keyword-only, in order to minimize complexity, but you don't always have the luxury of changing the implementation.Now of course you can't just change the parameter rules for overloads, since they're built into the parser. But a pragmatic workaround would be to add something like
ValueRequired
as a sentinel value to mark an argument in an overload as required in order to match that overload, but it may either match positionally or by keyword:Or maybe as a generic type so the type checker does not have to special case the default value, similar to
NotRequired
forTypedDict
.Beta Was this translation helpful? Give feedback.
All reactions