[ty] don't union in default type for annotated parameters (#21208)

This commit is contained in:
Carl Meyer 2025-11-02 18:21:54 -05:00 committed by GitHub
parent c32234cf0d
commit 0454a72674
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 21 additions and 40 deletions

View file

@ -1,12 +1,9 @@
# Function parameter types
Within a function scope, the declared type of each parameter is its annotated type (or Unknown if
not annotated). The initial inferred type is the union of the declared type with the type of the
default value expression (if any). If both are fully static types, this union should simplify to the
annotated type (since the default value type must be assignable to the annotated type, and for fully
static types this means subtype-of, which simplifies in unions). But if the annotated type is
Unknown or another non-fully-static type, the default value type may still be relevant as lower
bound.
not annotated). The initial inferred type is the annotated type of the parameter, if any. If there
is no annotation, it is the union of `Unknown` with the type of the default value expression (if
any).
The variadic parameter is a variadic tuple of its annotated type; the variadic-keywords parameter is
a dictionary from strings to its annotated type.
@ -41,13 +38,13 @@ def g(*args, **kwargs):
## Annotation is present but not a fully static type
The default value type should be a lower bound on the inferred type.
If there is an annotation, we respect it fully and don't union in the default value type.
```py
from typing import Any
def f(x: Any = 1):
reveal_type(x) # revealed: Any | Literal[1]
reveal_type(x) # revealed: Any
```
## Default value type must be assignable to annotated type
@ -64,7 +61,7 @@ def f(x: int = "foo"):
from typing import Any
def g(x: Any = "foo"):
reveal_type(x) # revealed: Any | Literal["foo"]
reveal_type(x) # revealed: Any
```
## Stub functions

View file

@ -99,7 +99,7 @@ static_assert(is_assignable_to(int, Unknown))
def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None:
reveal_type(x) # revealed: Unknown
reveal_type(y) # revealed: tuple[str, Unknown]
reveal_type(z) # revealed: Unknown | Literal[1]
reveal_type(z) # revealed: Unknown
```
`Unknown` can be subclassed, just like `Any`:

View file

@ -2423,15 +2423,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
///
/// The declared type is the annotated type, if any, or `Unknown`.
///
/// The inferred type is the annotated type, unioned with the type of the default value, if
/// any. If both types are fully static, this union is a no-op (it should simplify to just the
/// annotated type.) But in a case like `f(x=None)` with no annotated type, we want to infer
/// the type `Unknown | None` for `x`, not just `Unknown`, so that we can error on usage of `x`
/// that would not be valid for `None`.
///
/// If the default-value type is not assignable to the declared (annotated) type, we ignore the
/// default-value type and just infer the annotated type; this is the same way we handle
/// assignments, and allows an explicit annotation to override a bad inference.
/// The inferred type is the annotated type, if any. If there is no annotation, it is the union
/// of `Unknown` and the type of the default value, if any.
///
/// Parameter definitions are odd in that they define a symbol in the function-body scope, so
/// the Definition belongs to the function body scope, but the expressions (annotation and
@ -2460,23 +2453,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.map(|default| self.file_expression_type(default));
if let Some(annotation) = parameter.annotation.as_ref() {
let declared_ty = self.file_expression_type(annotation);
let declared_and_inferred_ty = if let Some(default_ty) = default_ty {
if default_ty.is_assignable_to(self.db(), declared_ty) {
DeclaredAndInferredType::MightBeDifferent {
declared_ty: TypeAndQualifiers::declared(declared_ty),
inferred_ty: UnionType::from_elements(self.db(), [declared_ty, default_ty]),
}
} else if (self.in_stub()
|| self.in_function_overload_or_abstractmethod()
|| self
.class_context_of_current_method()
.is_some_and(|class| class.is_protocol(self.db())))
&& default
.as_ref()
.is_some_and(|d| d.is_ellipsis_literal_expr())
if let Some(default_ty) = default_ty {
if !default_ty.is_assignable_to(self.db(), declared_ty)
&& !((self.in_stub()
|| self.in_function_overload_or_abstractmethod()
|| self
.class_context_of_current_method()
.is_some_and(|class| class.is_protocol(self.db())))
&& default
.as_ref()
.is_some_and(|d| d.is_ellipsis_literal_expr()))
{
DeclaredAndInferredType::are_the_same_type(declared_ty)
} else {
if let Some(builder) = self
.context
.report_lint(&INVALID_PARAMETER_DEFAULT, parameter_with_default)
@ -2488,15 +2475,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
declared_ty.display(self.db())
));
}
DeclaredAndInferredType::are_the_same_type(declared_ty)
}
} else {
DeclaredAndInferredType::are_the_same_type(declared_ty)
};
}
self.add_declaration_with_binding(
parameter.into(),
definition,
&declared_and_inferred_ty,
&DeclaredAndInferredType::are_the_same_type(declared_ty),
);
} else {
let ty = if let Some(default_ty) = default_ty {