Special-case type return

This commit is contained in:
Charlie Marsh
2025-12-31 13:35:52 -05:00
parent ebe4f00b82
commit 6ea342e548
2 changed files with 49 additions and 3 deletions

View File

@@ -115,6 +115,40 @@ Foo() # error: [missing-argument]
reveal_type(Foo(1)) # revealed: Foo
```
### Metaclass `__call__` returning bare `type`
When the metaclass `__call__` is annotated as returning `type`, this is typically a mistake in
singleton patterns where the programmer intended to return an instance. Both mypy and pyright handle
this specially by ignoring the `type` return annotation and using the instance type instead.
```py
from typing import Any
class Singleton(type):
_instances: dict["Singleton", object] = {}
def __call__(cls, *args: Any, **kwargs: Any) -> type:
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
# error: [invalid-return-type]
return cls._instances[cls]
class MyConfig(metaclass=Singleton):
def __init__(self, x: int) -> None:
pass
def get(self, key: str) -> str:
return key
# Despite the `-> type` annotation, we treat this as returning an instance.
# This matches mypy and pyright behavior for this common pattern.
MyConfig() # error: [missing-argument]
reveal_type(MyConfig(1)) # revealed: MyConfig
# Instance methods work correctly.
MyConfig(1).get("key")
```
## Default
```py

View File

@@ -7349,11 +7349,23 @@ impl<'db> Type<'db> {
.to_instance(db)
.expect("type should be convertible to instance type");
// Special case: if the return type is exactly `type`, treat it as returning the
// instance type. This is a common pattern in singleton metaclasses where `__call__`
// is annotated as `-> type` but actually returns an instance. Both mypy and pyright
// handle this specially to avoid false positives.
// Note: `-> type` in annotations becomes `NominalInstance(type)`.
let returns_bare_type = matches!(
metaclass_return_type,
Type::NominalInstance(instance) if instance.class(db).is_known(db, KnownClass::Type)
);
// Check if we should skip `__new__`/`__init__` evaluation.
// Skip if: return type is not assignable to instance, is Never, or contains Any.
let skip_new_init = !metaclass_return_type.is_assignable_to(db, instance_ty)
|| metaclass_return_type.is_never()
|| matches!(metaclass_return_type, Type::Dynamic(DynamicType::Any));
// But don't skip if the return type is exactly `type` (common singleton pattern).
let skip_new_init = !returns_bare_type
&& (!metaclass_return_type.is_assignable_to(db, instance_ty)
|| metaclass_return_type.is_never()
|| matches!(metaclass_return_type, Type::Dynamic(DynamicType::Any)));
// If there are argument errors or we should skip `__new__`/`__init__`, return metaclass result.
if call_result.is_err() || skip_new_init {