Skip to content
57 changes: 43 additions & 14 deletions crates/red_knot_python_semantic/resources/mdtest/annotations/any.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,42 +46,71 @@ def f():
y: Any = "not an Any" # error: [invalid-assignment]
```

## Subclass
## Subclasses of `Any`

The spec allows you to define subclasses of `Any`.

`Subclass` has an unknown superclass, which might be `int`. The assignment to `x` should not be
`SubclassOfAny` has an unknown superclass, which might be `int`. The assignment to `x` should not be
allowed, even when the unknown superclass is `int`. The assignment to `y` should be allowed, since
`Subclass` might have `int` as a superclass, and is therefore assignable to `int`.

```py
from typing import Any

class Subclass(Any): ...
class SubclassOfAny(Any): ...

reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
reveal_type(SubclassOfAny.__mro__) # revealed: tuple[Literal[SubclassOfAny], Any, Literal[object]]

x: Subclass = 1 # error: [invalid-assignment]
y: int = Subclass()

def _(s: Subclass):
reveal_type(s) # revealed: Subclass
x: SubclassOfAny = 1 # error: [invalid-assignment]
y: int = SubclassOfAny()
```

`Subclass` should not be assignable to a final class though, because `Subclass` could not possibly
be a subclass of `FinalClass`:
`SubclassOfAny` should not be assignable to a final class though, because `SubclassOfAny` could not
possibly be a subclass of `FinalClass`:

```py
from typing import final

@final
class FinalClass: ...

f: FinalClass = Subclass() # error: [invalid-assignment]
f: FinalClass = SubclassOfAny() # error: [invalid-assignment]

@final
class OtherFinalClass: ...

f: FinalClass | OtherFinalClass = SubclassOfAny() # error: [invalid-assignment]
```

A subclass of `Any` can also be assigned to arbitrary `Callable` types:

```py
from typing import Callable, Any

def takes_callable1(f: Callable):
f()

takes_callable1(SubclassOfAny())

def takes_callable2(f: Callable[[int], None]):
f(1)

takes_callable2(SubclassOfAny())
```

A subclass of `Any` cannot be assigned to literal types, since those can not be subclassed:

```py
from typing import Any, Literal

class MockAny(Any):
pass

x: Literal[1] = MockAny() # error: [invalid-assignment]
```

A use case where this comes up is with mocking libraries, where the mock object should be assignable
to any type:
A use case where subclasses of `Any` come up is in mocking libraries, where the mock object should
be assignable to (almost) any type:

```py
from unittest.mock import MagicMock
Expand Down
6 changes: 6 additions & 0 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1477,6 +1477,12 @@ impl<'db> Type<'db> {
self_callable.is_assignable_to(db, target_callable)
}

(Type::NominalInstance(instance), Type::Callable(_))
if instance.class().is_subclass_of_any_or_unknown(db) =>
{
true
}

(Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
let call_symbol = self.member(db, "__call__").symbol;
match call_symbol {
Expand Down
18 changes: 11 additions & 7 deletions crates/red_knot_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,16 @@ impl<'db> ClassType<'db> {
class_literal.is_final(db)
}

/// Is this class a subclass of `Any` or `Unknown`?
pub(crate) fn is_subclass_of_any_or_unknown(self, db: &'db dyn Db) -> bool {
self.iter_mro(db).any(|base| {
matches!(
base,
ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown)
)
})
}

/// If `self` and `other` are generic aliases of the same generic class, returns their
/// corresponding specializations.
fn compatible_specializations(
Expand Down Expand Up @@ -274,13 +284,7 @@ impl<'db> ClassType<'db> {
}
}

if self.iter_mro(db).any(|base| {
matches!(
base,
ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown)
)
}) && !other.is_final(db)
{
if self.is_subclass_of_any_or_unknown(db) && !other.is_final(db) {
return true;
}

Expand Down
Loading