I’ve floated around some ideas about this previously in python/typing and a GitHub issue that was caused by another shortcoming of the current way we specify function overloads.
There are currently two major annoyances I’ve come across multiple times when writing overloads:
-
Optional arguments that are both keyword and positional can get annoying to deal with very quickly because you often will find yourself writing the same overload twice (once for the positional version and once for the keyword version) because we’re not allowed to have optional arguments follow required ones (unless they are keyword-only).
-
While it’s obvious that overloads should be matched in the order they are defined in, it doesn’t always make sense to use the first overload as the unsolved case (i.e. the overload that is picked when the type solver can’t pick a specific one). This is somewhat similar to an unsolved
TypeVar
, although type checkers seem to have decided that it’s better to pick one of the overloads than to try to carry around an unsolved overload. This will often result in false positives. Sometimes the simple solution is to change the order of the overloads, but for overlapping overloads this may not always be possible, so we’re forced to decrease the accuracy of our overloads and contend with false negatives instead.
I would be interested in exploring if we can come up with some small extensions to the current typing constructs that would solve the above issues (and any others we can come up with) with overloads and collect them in a new PEP.
For 1) I have suggested either re-using typing.Required
to mark a parameter as faux-required in an overload, i.e. a type checking only marker that has no effect on the runtime behavior of the function, or to add something like a RequiredParameter
sentinel value that can be set as the default and means the same thing.
@overload
def foo(x: int = ..., y: Required[int] = ..., z: int = ...) -> float: ...
@overload
def foo(x: int = ..., y: None = ..., z: int = ...) -> float | None: ...
For 2) I have suggested something like a @default_overload
that behaves like a regular @overload
but type checkers will default to this overload, rather than the first one, if they can’t solve the overload.
@overload
def foo(x: LiteralString) -> LiteralString: ...
@default_overload
def foo(x: str) -> str: ...