This thread is an attempt to clarify another ambiguous part of the typing spec that affects overload resolution.
Consider a function whose signature includes a parameter with a type annotation and a default value whose type is not assignable to the annotated type.
def func(x: int = "") -> None: ...
This is clearly a type violation, and type checkers should report an error here. However, it’s less clear what a type checker should do in the case where code calls this function.
# This is clearly OK
func(1)
# Should this result in an error?
# Older versions of pyright (<1.1.395) do not unless
# the parameter type includes a type variable.
# The latest version of pyright (1.1.395) does.
# Mypy does not.
func()
This question is not currently answered by the typing spec. However, the answer potentially impacts overload resolution behaviors, which we’ve been trying to standardize. I’d like to see if we can achieve consensus on the preferred behavior.
Here are some additional considerations.
- If the annotated type of the parameter is a bare type variable or a type that includes a type variable, it arguably becomes more important to verify assignability because the type variable cannot be solved if assignment isn’t possible.
# Mypy doesn't currently support this use case, but pyright does.
def without_bound[T](x: T = "") -> T: ...
reveal_type(without_bound()) # str
def with_bound[T: int](x: T = "") -> T: ... # Error
reveal_type(with_bound()) # Error?
- I still occasionally encounter some libraries that use the old practice of specifying a default value of
Noneto imply that the parameter type isOptional. This behavior has been deprecated for nearly five years in PEP 484, but pyright and mypy both provide a backward-compatibility setting to enable it. If a library uses this outdated practice, a consumer of the library may see errors in their code that cannot be cleanly mitigated without a library update or without enabling this backward-compatibility setting for their code base.
# Library code
def func(x: int = None): ...
# User code
from library import func
func() # Error?
- When an ellipsis token (
...) appears within a value expression, it is generally assumed to take on its runtime value whose type isEllipsisType. The typing spec specifically defines several situations where a type checker should treat...not as a literal ellipsis but should instead apply a different meaning. Most relevant to this discussion, the spec says that a...that appears within any value expression slot within a stub file should be treated as “any value” (i.e. anAnytype). It is common practice for default argument values to use...within stub files. But what if...is used in this manner within a regular source (“.py”) file? For example, what if it’s used in an overload signature or protocol definition? How should...be interpreted in this case?
# test.py
def func1[T](x: T = ...) -> T: ...
reveal_type(func1()) # EllipsisType? Any?
@overload
def func2(x: str) -> str: ...
@overload
# In the following line, does ... mean a literal ellipsis or "a value with any type"?
def func2(x: EllipsisType = ...) -> EllipsisType: ...
def func2(x: str | EllipsisType = ...) -> str | EllipsisType: ...
This distinction is important because some libraries (most notably pandas) are using ... in overload definitions within “.py” files, and it appears that their intent is for ... to have the same meaning as in a stub file.
In summary, I’m looking for clarity on two related issues:
- Should a type checker emit an error for a call expression that targets a callable whose default argument type doesn’t match the annotated type for the corresponding parameter? If so, should this be considered an error for purposes of rejecting an overload signature during the overload resolution process?
- When
...is used for a default argument value expression outside of a stub file, should it be treated the same as if it appears within a stub file? Under which conditions?
I don’t have strong opinions on either of these, but my slight inclination is that type checkers should emit an error for this condition, and ... should be interpreted as the literal ellipsis value when it’s used in a default argument expression outside of a stub file.