Repository: astral-sh/ruff
Pull request #17682: [red-knot] Add a new `Type::ProtocolInstance` variant (take 2!)
Author: login: AlexWaygood
Labels: name: ty
description: Multi-file analysis & type inference
Created: 2025-04-28T15:00:34Z | Merged: 2025-04-30T11:03:11Z

## Summary

This PR adds initial support for protocol types to red-knot.

Specifically, it does the following things:
- Adds a new `Type::ProtocolInstance` variant. A `ProtocolInstanceType` represents "the set of all possible runtime objects that would satisfy a given protocol".
- Renames `Type::Instance`/`InstanceType` to `Type::NominalInstance`/`NominalInstanceType`, to differentiate them from `Type::ProtocolInstance`/`ProtocolInstanceType`.
- Reworks the `Type::instance()` constructor: it now checks whether the class passed into the constructor is a protocol class. If it is, it returns a `Type::ProtocolInstance()`; if not, it returns a `Type::NominalInstance`
- Implements naive versions of subtyping, assignability and equivalence for protocol types, both relative to other protocol types and relative to other `Type` variants altogether.
- Removes hardcoded special-casing for `typing.SupportsIndex` and `typing.Sized`. This special-casing is now redundant.
- Removes the `KnownClass::Sized` variant altogether: its only purpose was to achieve the special-cased support for this protocol in red-knot.

There is still much to be done, and many TODOs remain. The most obvious piece of outstanding work is that we still do not consider the _types_ of the members declared on a protocol class when we consider whether another type is assignable to a protocol type. I.e., this assertion currently fails on this branch, when it should pass due to the fact that `Bar.x` has type `str` but `Foo` demands that inhabitants of the type must have an `x` attribute of type `int`:

```py
from typing import Protocol
from knot_extensions import static_assert, is_assignable_to

class Foo(Protocol):
    x: int

class Bar:
    x = "foo"

static_assert(not is_assignable_to(Bar, Foo))
```

As such, this PR gets rid of many false-positive diagnostics associated with protocols. But it comes at the cost of introducing many potential false negatives. These will be tackled in followup PRs.

## Test Plan

- Existing mdtests updated
- New mdtests added for issues that were surfaced via mypy_primer on early versions of this branch
- Unit test added for the `Display` implementation of synthesized protocols
- Ran `uvx --from=./python/py-fuzzer fuzz 0-2000 --only-new-bugs  --baseline-executable=./target/main/debug/red_knot --bin=red_knot` to verify that this branch doesn't introduce any new panics detectable by the `py-fuzzer` script
- Ran `QUICKCHECK_TESTS=100000 cargo test --release -p red_knot_python_semantic -- --ignored types::property_tests::stable` to check that the property tests still pass
  - (This PR doesn't yet add the machinery to the property tests so that they would ever actually test any protocol types, so this is unsurprising. But I double-checked anyway.)
- The mypy_primer report looks excellent: all hits appear to either be false positives going away, or pre-existing error messages changing slightly



Diff:
diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/builtins.md b/crates/red_knot_python_semantic/resources/mdtest/import/builtins.md
index 1b2305fb410c2f..7d1a3f5e0bd49a 100644
--- a/crates/red_knot_python_semantic/resources/mdtest/import/builtins.md
+++ b/crates/red_knot_python_semantic/resources/mdtest/import/builtins.md
@@ -7,7 +7,7 @@ Builtin symbols can be explicitly imported:
 ```py
 import builtins
 
-reveal_type(builtins.chr)  # revealed: def chr(i: int | SupportsIndex, /) -> str
+reveal_type(builtins.chr)  # revealed: def chr(i: SupportsIndex, /) -> str
 ```
 
 ## Implicit use of builtin
@@ -15,7 +15,7 @@ reveal_type(builtins.chr)  # revealed: def chr(i: int | SupportsIndex, /) -> str
 Or used implicitly:
 
 ```py
-reveal_type(chr)  # revealed: def chr(i: int | SupportsIndex, /) -> str
+reveal_type(chr)  # revealed: def chr(i: SupportsIndex, /) -> str
 reveal_type(str)  # revealed: Literal[str]
 ```
 
diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md
index 0af5a8b58cdd95..5f124e0549508f 100644
--- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md
+++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md
@@ -230,7 +230,7 @@ And it is also an error to use `Protocol` in type expressions:
 def f(
     x: Protocol,  # error: [invalid-type-form] "`typing.Protocol` is not allowed in type expressions"
     y: type[Protocol],  # TODO: should emit `[invalid-type-form]` here too
-) -> None:
+):
     reveal_type(x)  # revealed: Unknown
 
     # TODO: should be `type[Unknown]`
@@ -266,9 +266,7 @@ class Bar(typing_extensions.Protocol):
 
 static_assert(typing_extensions.is_protocol(Foo))
 static_assert(typing_extensions.is_protocol(Bar))
-
-# TODO: should pass
-static_assert(is_equivalent_to(Foo, Bar))  # error: [static-assert-error]
+static_assert(is_equivalent_to(Foo, Bar))
 ```
 
 The same goes for `typing.runtime_checkable` and `typing_extensions.runtime_checkable`:
@@ -284,9 +282,7 @@ class RuntimeCheckableBar(typing_extensions.Protocol):
 
 static_assert(typing_extensions.is_protocol(RuntimeCheckableFoo))
 static_assert(typing_extensions.is_protocol(RuntimeCheckableBar))
-
-# TODO: should pass
-static_assert(is_equivalent_to(RuntimeCheckableFoo, RuntimeCheckableBar))  # error: [static-assert-error]
+static_assert(is_equivalent_to(RuntimeCheckableFoo, RuntimeCheckableBar))
 
 # These should not error because the protocols are decorated with `@runtime_checkable`
 isinstance(object(), RuntimeCheckableFoo)
@@ -488,21 +484,20 @@ class HasX(Protocol):
 class Foo:
     x: int
 
-# TODO: these should pass
-static_assert(is_subtype_of(Foo, HasX))  # error: [static-assert-error]
-static_assert(is_assignable_to(Foo, HasX))  # error: [static-assert-error]
+static_assert(is_subtype_of(Foo, HasX))
+static_assert(is_assignable_to(Foo, HasX))
 
 class FooSub(Foo): ...
 
-# TODO: these should pass
-static_assert(is_subtype_of(FooSub, HasX))  # error: [static-assert-error]
-static_assert(is_assignable_to(FooSub, HasX))  # error: [static-assert-error]
+static_assert(is_subtype_of(FooSub, HasX))
+static_assert(is_assignable_to(FooSub, HasX))
 
 class Bar:
     x: str
 
-static_assert(not is_subtype_of(Bar, HasX))
-static_assert(not is_assignable_to(Bar, HasX))
+# TODO: these should pass
+static_assert(not is_subtype_of(Bar, HasX))  # error: [static-assert-error]
+static_assert(not is_assignable_to(Bar, HasX))  # error: [static-assert-error]
 
 class Baz:
     y: int
@@ -524,14 +519,16 @@ class A:
     def x(self) -> int:
         return 42
 
-static_assert(not is_subtype_of(A, HasX))
-static_assert(not is_assignable_to(A, HasX))
+# TODO: these should pass
+static_assert(not is_subtype_of(A, HasX))  # error: [static-assert-error]
+static_assert(not is_assignable_to(A, HasX))  # error: [static-assert-error]
 
 class B:
     x: Final = 42
 
-static_assert(not is_subtype_of(A, HasX))
-static_assert(not is_assignable_to(A, HasX))
+# TODO: these should pass
+static_assert(not is_subtype_of(A, HasX))  # error: [static-assert-error]
+static_assert(not is_assignable_to(A, HasX))  # error: [static-assert-error]
 
 class IntSub(int): ...
 
@@ -541,8 +538,10 @@ class C:
 # due to invariance, a type is only a subtype of `HasX`
 # if its `x` attribute is of type *exactly* `int`:
 # a subclass of `int` does not satisfy the interface
-static_assert(not is_subtype_of(C, HasX))
-static_assert(not is_assignable_to(C, HasX))
+#
+# TODO: these should pass
+static_assert(not is_subtype_of(C, HasX))  # error: [static-assert-error]
+static_assert(not is_assignable_to(C, HasX))  # error: [static-assert-error]
 ```
 
 All attributes on frozen dataclasses and namedtuples are immutable, so instances of these classes
@@ -556,22 +555,23 @@ from typing import NamedTuple
 class MutableDataclass:
     x: int
 
-# TODO: these should pass
-static_assert(is_subtype_of(MutableDataclass, HasX))  # error: [static-assert-error]
-static_assert(is_assignable_to(MutableDataclass, HasX))  # error: [static-assert-error]
+static_assert(is_subtype_of(MutableDataclass, HasX))
+static_assert(is_assignable_to(MutableDataclass, HasX))
 
 @dataclass(frozen=True)
 class ImmutableDataclass:
     x: int
 
-static_assert(not is_subtype_of(ImmutableDataclass, HasX))
-static_assert(not is_assignable_to(ImmutableDataclass, HasX))
+# TODO: these should pass
+static_assert(not is_subtype_of(ImmutableDataclass, HasX))  # error: [static-assert-error]
+static_assert(not is_assignable_to(ImmutableDataclass, HasX))  # error: [static-assert-error]
 
 class NamedTupleWithX(NamedTuple):
     x: int
 
-static_assert(not is_subtype_of(NamedTupleWithX, HasX))
-static_assert(not is_assignable_to(NamedTupleWithX, HasX))
+# TODO: these should pass
+static_assert(not is_subtype_of(NamedTupleWithX, HasX))  # error: [static-assert-error]
+static_assert(not is_assignable_to(NamedTupleWithX, HasX))  # error: [static-assert-error]
 ```
 
 However, a type with a read-write property `x` *does* satisfy the `HasX` protocol. The `HasX`
@@ -590,9 +590,8 @@ class XProperty:
     def x(self, x: int) -> None:
         self._x = x**2
 
-# TODO: these should pass
-static_assert(is_subtype_of(XProperty, HasX))  # error: [static-assert-error]
-static_assert(is_assignable_to(XProperty, HasX))  # error: [static-assert-error]
+static_assert(is_subtype_of(XProperty, HasX))
+static_assert(is_assignable_to(XProperty, HasX))
 ```
 
 Attribute members on protocol classes are allowed to have default values, just like instance
@@ -717,9 +716,8 @@ from typing import Protocol
 
 class UniversalSet(Protocol): ...
 
-# TODO: these should pass
-static_assert(is_assignable_to(object, UniversalSet))  # error: [static-assert-error]
-static_assert(is_subtype_of(object, UniversalSet))  # error: [static-assert-error]
+static_assert(is_assignable_to(object, UniversalSet))
+static_assert(is_subtype_of(object, UniversalSet))
 ```
 
 Which means that `UniversalSet` here is in fact an equivalent type to `object`:
@@ -727,8 +725,7 @@ Which means that `UniversalSet` here is in fact an equivalent type to `object`:
 ```py
 from knot_extensions import is_equivalent_to
 
-# TODO: this should pass
-static_assert(is_equivalent_to(UniversalSet, object))  # error: [static-assert-error]
+static_assert(is_equivalent_to(UniversalSet, object))
 ```
 
 `object` is a subtype of certain other protocols too. Since all fully static types (whether nominal
@@ -739,17 +736,16 @@ means that these protocols are also equivalent to `UniversalSet` and `object`:
 class SupportsStr(Protocol):
     def __str__(self) -> str: ...
 
-# TODO: these should pass
-static_assert(is_equivalent_to(SupportsStr, UniversalSet))  # error: [static-assert-error]
-static_assert(is_equivalent_to(SupportsStr, object))  # error: [static-assert-error]
+static_assert(is_equivalent_to(SupportsStr, UniversalSet))
+static_assert(is_equivalent_to(SupportsStr, object))
 
 class SupportsClass(Protocol):
-    __class__: type
+    @property
+    def __class__(self) -> type: ...
 
-# TODO: these should pass
-static_assert(is_equivalent_to(SupportsClass, UniversalSet))  # error: [static-assert-error]
-static_assert(is_equivalent_to(SupportsClass, SupportsStr))  # error: [static-assert-error]
-static_assert(is_equivalent_to(SupportsClass, object))  # error: [static-assert-error]
+static_assert(is_equivalent_to(SupportsClass, UniversalSet))
+static_assert(is_equivalent_to(SupportsClass, SupportsStr))
+static_assert(is_equivalent_to(SupportsClass, object))
 ```
 
 If a protocol contains members that are not defined on `object`, then that protocol will (like all
@@ -786,8 +782,7 @@ class HasX(Protocol):
 class AlsoHasX(Protocol):
     x: int
 
-# TODO: this should pass
-static_assert(is_equivalent_to(HasX, AlsoHasX))  # error: [static-assert-error]
+static_assert(is_equivalent_to(HasX, AlsoHasX))
 ```
 
 And unions containing equivalent protocols are recognised as equivalent, even when the order is not
@@ -803,8 +798,7 @@ class AlsoHasY(Protocol):
 class A: ...
 class B: ...
 
-# TODO: this should pass
-static_assert(is_equivalent_to(A | HasX | B | HasY, B | AlsoHasY | AlsoHasX | A))  # error: [static-assert-error]
+static_assert(is_equivalent_to(A | HasX | B | HasY, B | AlsoHasY | AlsoHasX | A))
 ```
 
 ## Intersections of protocols
@@ -882,9 +876,9 @@ from knot_extensions import is_subtype_of, is_assignable_to, static_assert, Type
 class HasX(Protocol):
     x: int
 
-# TODO: these should pass
+# TODO: this should pass
 static_assert(is_subtype_of(TypeOf[module], HasX))  # error: [static-assert-error]
-static_assert(is_assignable_to(TypeOf[module], HasX))  # error: [static-assert-error]
+static_assert(is_assignable_to(TypeOf[module], HasX))
 
 class ExplicitProtocolSubtype(HasX, Protocol):
     y: int
@@ -896,9 +890,8 @@ class ImplicitProtocolSubtype(Protocol):
     x: int
     y: str
 
-# TODO: these should pass
-static_assert(is_subtype_of(ImplicitProtocolSubtype, HasX))  # error: [static-assert-error]
-static_assert(is_assignable_to(ImplicitProtocolSubtype, HasX))  # error: [static-assert-error]
+static_assert(is_subtype_of(ImplicitProtocolSubtype, HasX))
+static_assert(is_assignable_to(ImplicitProtocolSubtype, HasX))
 
 class Meta(type):
     x: int
@@ -933,23 +926,24 @@ def f(obj: ClassVarXProto):
 class InstanceAttrX:
     x: int
 
-static_assert(not is_assignable_to(InstanceAttrX, ClassVarXProto))
-static_assert(not is_subtype_of(InstanceAttrX, ClassVarXProto))
+# TODO: these should pass
+static_assert(not is_assignable_to(InstanceAttrX, ClassVarXProto))  # error: [static-assert-error]
+static_assert(not is_subtype_of(InstanceAttrX, ClassVarXProto))  # error: [static-assert-error]
 
 class PropertyX:
     @property
     def x(self) -> int:
         return 42
 
-static_assert(not is_assignable_to(PropertyX, ClassVarXProto))
-static_assert(not is_subtype_of(PropertyX, ClassVarXProto))
+# TODO: these should pass
+static_assert(not is_assignable_to(PropertyX, ClassVarXProto))  # error: [static-assert-error]
+static_assert(not is_subtype_of(PropertyX, ClassVarXProto))  # error: [static-assert-error]
 
 class ClassVarX:
     x: ClassVar[int] = 42
 
-# TODO: these should pass
-static_assert(is_assignable_to(ClassVarX, ClassVarXProto))  # error: [static-assert-error]
-static_assert(is_subtype_of(ClassVarX, ClassVarXProto))  # error: [static-assert-error]
+static_assert(is_assignable_to(ClassVarX, ClassVarXProto))
+static_assert(is_subtype_of(ClassVarX, ClassVarXProto))
 ```
 
 This is mentioned by the
@@ -976,18 +970,16 @@ class HasXProperty(Protocol):
 class XAttr:
     x: int
 
-# TODO: these should pass
-static_assert(is_subtype_of(XAttr, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XAttr, HasXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XAttr, HasXProperty))
+static_assert(is_assignable_to(XAttr, HasXProperty))
 
 class XReadProperty:
     @property
     def x(self) -> int:
         return 42
 
-# TODO: these should pass
-static_assert(is_subtype_of(XReadProperty, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XReadProperty, HasXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XReadProperty, HasXProperty))
+static_assert(is_assignable_to(XReadProperty, HasXProperty))
 
 class XReadWriteProperty:
     @property
@@ -997,22 +989,20 @@ class XReadWriteProperty:
     @x.setter
     def x(self, val: int) -> None: ...
 
-# TODO: these should pass
-static_assert(is_subtype_of(XReadWriteProperty, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XReadWriteProperty, HasXProperty))
+static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))
 
 class XClassVar:
     x: ClassVar[int] = 42
 
-static_assert(is_subtype_of(XClassVar, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XClassVar, HasXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XClassVar, HasXProperty))
+static_assert(is_assignable_to(XClassVar, HasXProperty))
 
 class XFinal:
     x: Final = 42
 
-# TODO: these should pass
-static_assert(is_subtype_of(XFinal, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XFinal, HasXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XFinal, HasXProperty))
+static_assert(is_assignable_to(XFinal, HasXProperty))
 ```
 
 A read-only property on a protocol, unlike a mutable attribute, is covariant: `XSub` in the below
@@ -1025,9 +1015,8 @@ class MyInt(int): ...
 class XSub:
     x: MyInt
 
-# TODO: these should pass
-static_assert(is_subtype_of(XSub, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XSub, HasXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XSub, HasXProperty))
+static_assert(is_assignable_to(XSub, HasXProperty))
 ```
 
 A read/write property on a protocol, where the getter returns the same type that the setter takes,
@@ -1043,17 +1032,17 @@ class HasMutableXProperty(Protocol):
 class XAttr:
     x: int
 
-# TODO: these should pass
-static_assert(is_subtype_of(XAttr, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XAttr, HasXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XAttr, HasXProperty))
+static_assert(is_assignable_to(XAttr, HasXProperty))
 
 class XReadProperty:
     @property
     def x(self) -> int:
         return 42
 
-static_assert(not is_subtype_of(XReadProperty, HasXProperty))
-static_assert(not is_assignable_to(XReadProperty, HasXProperty))
+# TODO: these should pass
+static_assert(not is_subtype_of(XReadProperty, HasXProperty))  # error: [static-assert-error]
+static_assert(not is_assignable_to(XReadProperty, HasXProperty))  # error: [static-assert-error]
 
 class XReadWriteProperty:
     @property
@@ -1063,15 +1052,15 @@ class XReadWriteProperty:
     @x.setter
     def x(self, val: int) -> None: ...
 
-# TODO: these should pass
-static_assert(is_subtype_of(XReadWriteProperty, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XReadWriteProperty, HasXProperty))
+static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))
 
 class XSub:
     x: MyInt
 
-static_assert(not is_subtype_of(XSub, HasXProperty))
-static_assert(not is_assignable_to(XSub, HasXProperty))
+# TODO: should pass
+static_assert(not is_subtype_of(XSub, HasXProperty))  # error: [static-assert-error]
+static_assert(not is_assignable_to(XSub, HasXProperty))  # error: [static-assert-error]
 ```
 
 A protocol with a read/write property `x` is exactly equivalent to a protocol with a mutable
@@ -1083,16 +1072,13 @@ from knot_extensions import is_equivalent_to
 class HasMutableXAttr(Protocol):
     x: int
 
-# TODO: this should pass
-static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty))  # error: [static-assert-error]
+static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty))
 
-# TODO: these should pass
-static_assert(is_subtype_of(HasMutableXAttr, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(HasMutableXAttr, HasXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(HasMutableXAttr, HasXProperty))
+static_assert(is_assignable_to(HasMutableXAttr, HasXProperty))
 
-# TODO: these should pass
-static_assert(is_subtype_of(HasMutableXProperty, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(HasMutableXProperty, HasXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(HasMutableXProperty, HasXProperty))
+static_assert(is_assignable_to(HasMutableXProperty, HasXProperty))
 ```
 
 A read/write property on a protocol, where the setter accepts a subtype of the type returned by the
@@ -1119,9 +1105,8 @@ class HasAsymmetricXProperty(Protocol):
 class XAttr:
     x: int
 
-# TODO: these should pass
-static_assert(is_subtype_of(XAttr, HasAsymmetricXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XAttr, HasAsymmetricXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XAttr, HasAsymmetricXProperty))
+static_assert(is_assignable_to(XAttr, HasAsymmetricXProperty))
 ```
 
 The end conclusion of this is that the getter-returned type of a property is always covariant and
@@ -1132,9 +1117,8 @@ regular mutable attribute, where the implied getter-returned and setter-accepted
 class XAttrSub:
     x: MyInt
 
-# TODO: these should pass
-static_assert(is_subtype_of(XAttrSub, HasAsymmetricXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XAttrSub, HasAsymmetricXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XAttrSub, HasAsymmetricXProperty))
+static_assert(is_assignable_to(XAttrSub, HasAsymmetricXProperty))
 
 class MyIntSub(MyInt):
     pass
@@ -1142,8 +1126,9 @@ class MyIntSub(MyInt):
 class XAttrSubSub:
     x: MyIntSub
 
-static_assert(not is_subtype_of(XAttrSubSub, HasAsymmetricXProperty))
-static_assert(not is_assignable_to(XAttrSubSub, HasAsymmetricXProperty))
+# TODO: should pass
+static_assert(not is_subtype_of(XAttrSubSub, HasAsymmetricXProperty))  # error: [static-assert-error]
+static_assert(not is_assignable_to(XAttrSubSub, HasAsymmetricXProperty))  # error: [static-assert-error]
 ```
 
 An asymmetric property on a protocol can also be satisfied by an asymmetric property on a nominal
@@ -1159,9 +1144,8 @@ class XAsymmetricProperty:
     @x.setter
     def x(self, x: int) -> None: ...
 
-# TODO: these should pass
-static_assert(is_subtype_of(XAsymmetricProperty, HasAsymmetricXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XAsymmetricProperty, HasAsymmetricXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XAsymmetricProperty, HasAsymmetricXProperty))
+static_assert(is_assignable_to(XAsymmetricProperty, HasAsymmetricXProperty))
 ```
 
 A custom descriptor attribute on the nominal class will also suffice:
@@ -1176,9 +1160,8 @@ class Descriptor:
 class XCustomDescriptor:
     x: Descriptor = Descriptor()
 
-# TODO: these should pass
-static_assert(is_subtype_of(XCustomDescriptor, HasAsymmetricXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XCustomDescriptor, HasAsymmetricXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XCustomDescriptor, HasAsymmetricXProperty))
+static_assert(is_assignable_to(XCustomDescriptor, HasAsymmetricXProperty))
 ```
 
 Moreover, a read-only property on a protocol can be satisfied by a nominal class that defines a
@@ -1191,19 +1174,20 @@ class HasGetAttr:
     def __getattr__(self, attr: str) -> int:
         return 42
 
-# TODO: these should pass
-static_assert(is_subtype_of(HasGetAttr, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(HasGetAttr, HasXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(HasGetAttr, HasXProperty))
+static_assert(is_assignable_to(HasGetAttr, HasXProperty))
 
-static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr))
-static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr))
+# TODO: these should pass
+static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr))  # error: [static-assert-error]
+static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr))  # error: [static-assert-error]
 
 class HasGetAttrWithUnsuitableReturn:
     def __getattr__(self, attr: str) -> tuple[int, int]:
         return (1, 2)
 
-static_assert(not is_subtype_of(HasGetAttrWithUnsuitableReturn, HasXProperty))
-static_assert(not is_assignable_to(HasGetAttrWithUnsuitableReturn, HasXProperty))
+# TODO: these should pass
+static_assert(not is_subtype_of(HasGetAttrWithUnsuitableReturn, HasXProperty))  # error: [static-assert-error]
+static_assert(not is_assignable_to(HasGetAttrWithUnsuitableReturn, HasXProperty))  # error: [static-assert-error]
 
 class HasGetAttrAndSetAttr:
     def __getattr__(self, attr: str) -> MyInt:
@@ -1211,9 +1195,10 @@ class HasGetAttrAndSetAttr:
 
     def __setattr__(self, attr: str, value: int) -> None: ...
 
+static_assert(is_subtype_of(HasGetAttrAndSetAttr, HasXProperty))
+static_assert(is_assignable_to(HasGetAttrAndSetAttr, HasXProperty))
+
 # TODO: these should pass
-static_assert(is_subtype_of(HasGetAttrAndSetAttr, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(HasGetAttrAndSetAttr, HasXProperty))  # error: [static-assert-error]
 static_assert(is_subtype_of(HasGetAttrAndSetAttr, XAsymmetricProperty))  # error: [static-assert-error]
 static_assert(is_assignable_to(HasGetAttrAndSetAttr, XAsymmetricProperty))  # error: [static-assert-error]
 ```
@@ -1363,12 +1348,12 @@ from knot_extensions import is_subtype_of, is_assignable_to
 class NominalWithX:
     x: int = 42
 
-# TODO: these should pass
-static_assert(is_assignable_to(NominalWithX, FullyStatic))  # error: [static-assert-error]
-static_assert(is_assignable_to(NominalWithX, NotFullyStatic))  # error: [static-assert-error]
-static_assert(is_subtype_of(NominalWithX, FullyStatic))  # error: [static-assert-error]
+static_assert(is_assignable_to(NominalWithX, FullyStatic))
+static_assert(is_assignable_to(NominalWithX, NotFullyStatic))
+static_assert(is_subtype_of(NominalWithX, FullyStatic))
 
-static_assert(not is_subtype_of(NominalWithX, NotFullyStatic))
+# TODO: this should pass
+static_assert(not is_subtype_of(NominalWithX, NotFullyStatic))  # error: [static-assert-error]
 ```
 
 Empty protocols are fully static; this follows from the fact that an empty protocol is equivalent to
@@ -1418,30 +1403,124 @@ static_assert(not is_fully_static(NoParameterAnnotation))  # error: [static-asse
 static_assert(not is_fully_static(NoReturnAnnotation))  # error: [static-assert-error]
 ```
 
-## `typing.SupportsIndex` and `typing.Sized`
+## Callable protocols
 
-`typing.SupportsIndex` is already somewhat supported through some special-casing in red-knot.
+An instance of a protocol type is callable if the protocol defines a `__call__` method:
 
 ```py
-from typing import SupportsIndex, Literal
+from typing import Protocol
 
-def _(some_int: int, some_literal_int: Literal[1], some_indexable: SupportsIndex):
-    a: SupportsIndex = some_int
-    b: SupportsIndex = some_literal_int
-    c: SupportsIndex = some_indexable
+class CallMeMaybe(Protocol):
+    def __call__(self, x: int) -> str: ...
+
+def f(obj: CallMeMaybe):
+    reveal_type(obj(42))  # revealed: str
+    obj("bar")  # error: [invalid-argument-type]
+```
+
+An instance of a protocol like this can be assignable to a `Callable` type, but only if it has the
+right signature:
+
+```py
+from typing import Callable
+from knot_extensions import is_subtype_of, is_assignable_to, static_assert
+
+static_assert(is_subtype_of(CallMeMaybe, Callable[[int], str]))
+static_assert(is_assignable_to(CallMeMaybe, Callable[[int], str]))
+static_assert(not is_subtype_of(CallMeMaybe, Callable[[str], str]))
+static_assert(not is_assignable_to(CallMeMaybe, Callable[[str], str]))
+static_assert(not is_subtype_of(CallMeMaybe, Callable[[CallMeMaybe, int], str]))
+static_assert(not is_assignable_to(CallMeMaybe, Callable[[CallMeMaybe, int], str]))
+
+def g(obj: Callable[[int], str], obj2: CallMeMaybe, obj3: Callable[[str], str]):
+    obj = obj2
+    obj3 = obj2  # error: [invalid-assignment]
+```
+
+By the same token, a `Callable` type can also be assignable to a protocol-instance type if the
+signature implied by the `Callable` type is assignable to the signature of the `__call__` method
+specified by the protocol:
+
+```py
+class Foo(Protocol):
+    def __call__(self, x: int, /) -> str: ...
+
+# TODO: these fail because we don't yet understand that all `Callable` types have a `__call__` method,
+# and we therefore don't think that the `Callable` type is assignable to `Foo`. They should pass.
+static_assert(is_subtype_of(Callable[[int], str], Foo))  # error: [static-assert-error]
+static_assert(is_assignable_to(Callable[[int], str], Foo))  # error: [static-assert-error]
+
+static_assert(not is_subtype_of(Callable[[str], str], Foo))
+static_assert(not is_assignable_to(Callable[[str], str], Foo))
+static_assert(not is_subtype_of(Callable[[CallMeMaybe, int], str], Foo))
+static_assert(not is_assignable_to(Callable[[CallMeMaybe, int], str], Foo))
+
+def h(obj: Callable[[int], str], obj2: Foo, obj3: Callable[[str], str]):
+    # TODO: this fails because we don't yet understand that all `Callable` types have a `__call__` method,
+    # and we therefore don't think that the `Callable` type is assignable to `Foo`. It should pass.
+    obj2 = obj  # error: [invalid-assignment]
+
+    # This diagnostic is correct, however.
+    obj2 = obj3  # error: [invalid-assignment]
+```
+
+## Protocols are never singleton types, and are never single-valued types
+
+It *might* be possible to have a singleton protocol-instance type...?
+
+For example, `WeirdAndWacky` in the following snippet only has a single possible inhabitant: `None`!
+It is thus a singleton type. However, going out of our way to recognise it as such is probably not
+worth it. Such cases should anyway be exceedingly rare and/or contrived.
+
+```py
+from typing import Protocol, Callable
+from knot_extensions import is_singleton, is_single_valued
+
+class WeirdAndWacky(Protocol):
+    @property
+    def __class__(self) -> Callable[[], None]: ...
+
+reveal_type(is_singleton(WeirdAndWacky))  # revealed: Literal[False]
+reveal_type(is_single_valued(WeirdAndWacky))  # revealed: Literal[False]
 ```
 
-The same goes for `typing.Sized`:
+## Integration test: `typing.SupportsIndex` and `typing.Sized`
+
+`typing.SupportsIndex` and `typing.Sized` are two protocols that are very commonly used in the wild.
 
 ```py
-from typing import Sized
+from typing import SupportsIndex, Sized, Literal
+
+def one(some_int: int, some_literal_int: Literal[1], some_indexable: SupportsIndex):
+    a: SupportsIndex = some_int
+    b: SupportsIndex = some_literal_int
+    c: SupportsIndex = some_indexable
 
-def _(some_list: list, some_tuple: tuple[int, str], some_sized: Sized):
+def two(some_list: list, some_tuple: tuple[int, str], some_sized: Sized):
     a: Sized = some_list
     b: Sized = some_tuple
     c: Sized = some_sized
 ```
 
+## Regression test: narrowing with self-referential protocols
+
+This snippet caused us to panic on an early version of the implementation for protocols.
+
+```py
+from typing import Protocol
+
+class A(Protocol):
+    def x(self) -> "B | A": ...
+
+class B(Protocol):
+    def y(self): ...
+
+obj = something_unresolvable  # error: [unresolved-reference]
+reveal_type(obj)  # revealed: Unknown
+if isinstance(obj, (B, A)):
+    reveal_type(obj)  # revealed: (Unknown & B) | (Unknown & A)
+```
+
 ## TODO
 
 Add tests for:
diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/builtin.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/builtin.md
index 5aa4175f8e6591..fc2ca70f97bab2 100644
--- a/crates/red_knot_python_semantic/resources/mdtest/scopes/builtin.md
+++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/builtin.md
@@ -13,7 +13,7 @@ if returns_bool():
     chr: int = 1
 
 def f():
-    reveal_type(chr)  # revealed: int | (def chr(i: int | SupportsIndex, /) -> str)
+    reveal_type(chr)  # revealed: int | (def chr(i: SupportsIndex, /) -> str)
 ```
 
 ## Conditionally global or builtin, with annotation
@@ -28,5 +28,5 @@ if returns_bool():
     chr: int = 1
 
 def f():
-    reveal_type(chr)  # revealed: int | (def chr(i: int | SupportsIndex, /) -> str)
+    reveal_type(chr)  # revealed: int | (def chr(i: SupportsIndex, /) -> str)
 ```
diff --git a/crates/red_knot_python_semantic/src/symbol.rs b/crates/red_knot_python_semantic/src/symbol.rs
index 3be4ce8d02fc5f..b90103585abe63 100644
--- a/crates/red_knot_python_semantic/src/symbol.rs
+++ b/crates/red_knot_python_semantic/src/symbol.rs
@@ -1114,7 +1114,7 @@ mod tests {
     fn assert_bound_string_symbol<'db>(db: &'db dyn Db, symbol: Symbol<'db>) {
         assert!(matches!(
             symbol,
-            Symbol::Type(Type::Instance(_), Boundness::Bound)
+            Symbol::Type(Type::NominalInstance(_), Boundness::Bound)
         ));
         assert_eq!(symbol.expect_type(), KnownClass::Str.to_instance(db));
     }
diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs
index 1047e929f06b0d..826684beb100b7 100644
--- a/crates/red_knot_python_semantic/src/types.rs
+++ b/crates/red_knot_python_semantic/src/types.rs
@@ -51,7 +51,8 @@ pub(crate) use crate::types::narrow::infer_narrowing_constraint;
 use crate::types::signatures::{Parameter, ParameterForm, Parameters};
 use crate::{Db, FxOrderSet, Module, Program};
 pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass};
-pub(crate) use instance::InstanceType;
+use instance::Protocol;
+pub(crate) use instance::{NominalInstanceType, ProtocolInstanceType};
 pub(crate) use known_instance::KnownInstanceType;
 
 mod builder;
@@ -489,7 +490,10 @@ pub enum Type<'db> {
     SubclassOf(SubclassOfType<'db>),
     /// The set of Python objects with the given class in their __class__'s method resolution order.
     /// Construct this variant using the `Type::instance` constructor function.
-    Instance(InstanceType<'db>),
+    NominalInstance(NominalInstanceType<'db>),
+    /// The set of Python objects that conform to the interface described by a given protocol.
+    /// Construct this variant using the `Type::instance` constructor function.
+    ProtocolInstance(ProtocolInstanceType<'db>),
     /// A single Python object that requires special treatment in the type system
     KnownInstance(KnownInstanceType<'db>),
     /// An instance of `builtins.property`
@@ -524,7 +528,7 @@ pub enum Type<'db> {
     TypeVar(TypeVarInstance<'db>),
     // A bound super object like `super()` or `super(A, A())`
     // This type doesn't handle an unbound super object like `super(A)`; for that we just use
-    // a `Type::Instance` of `builtins.super`.
+    // a `Type::NominalInstance` of `builtins.super`.
     BoundSuper(BoundSuperType<'db>),
     // TODO protocols, overloads, generics
 }
@@ -557,17 +561,17 @@ impl<'db> Type<'db> {
     }
 
     fn is_none(&self, db: &'db dyn Db) -> bool {
-        self.into_instance()
+        self.into_nominal_instance()
             .is_some_and(|instance| instance.class().is_known(db, KnownClass::NoneType))
     }
 
     fn is_bool(&self, db: &'db dyn Db) -> bool {
-        self.into_instance()
+        self.into_nominal_instance()
             .is_some_and(|instance| instance.class().is_known(db, KnownClass::Bool))
     }
 
     pub fn is_notimplemented(&self, db: &'db dyn Db) -> bool {
-        self.into_instance().is_some_and(|instance| {
+        self.into_nominal_instance().is_some_and(|instance| {
             instance
                 .class()
                 .is_known(db, KnownClass::NotImplementedType)
@@ -575,7 +579,7 @@ impl<'db> Type<'db> {
     }
 
     pub fn is_object(&self, db: &'db dyn Db) -> bool {
-        self.into_instance()
+        self.into_nominal_instance()
             .is_some_and(|instance| instance.class().is_object(db))
     }
 
@@ -597,7 +601,7 @@ impl<'db> Type<'db> {
             | Self::BooleanLiteral(_)
             | Self::BytesLiteral(_)
             | Self::FunctionLiteral(_)
-            | Self::Instance(_)
+            | Self::NominalInstance(_)
             | Self::ModuleLiteral(_)
             | Self::ClassLiteral(_)
             | Self::KnownInstance(_)
@@ -681,6 +685,8 @@ impl<'db> Type<'db> {
                         .iter()
                         .any(|ty| ty.contains_todo(db))
             }
+
+            Self::ProtocolInstance(protocol) => protocol.contains_todo(),
         }
     }
 
@@ -894,6 +900,7 @@ impl<'db> Type<'db> {
     ///   as these are irrelevant to whether a callable type `X` is equivalent to a callable type `Y`.
     /// - Strips the types of default values from parameters in `Callable` types: only whether a parameter
     ///   *has* or *does not have* a default value is relevant to whether two `Callable` types  are equivalent.
+    /// - Converts class-based protocols into synthesized protocols
     #[must_use]
     pub fn normalized(self, db: &'db dyn Db) -> Self {
         match self {
@@ -901,8 +908,9 @@ impl<'db> Type<'db> {
             Type::Intersection(intersection) => Type::Intersection(intersection.normalized(db)),
             Type::Tuple(tuple) => Type::Tuple(tuple.normalized(db)),
             Type::Callable(callable) => Type::Callable(callable.normalized(db)),
+            Type::ProtocolInstance(protocol) => protocol.normalized(db),
             Type::LiteralString
-            | Type::Instance(_)
+            | Type::NominalInstance(_)
             | Type::PropertyInstance(_)
             | Type::AlwaysFalsy
             | Type::AlwaysTruthy
@@ -996,7 +1004,7 @@ impl<'db> Type<'db> {
             (_, Type::Never) => false,
 
             // Everything is a subtype of `object`.
-            (_, Type::Instance(instance)) if instance.class().is_object(db) => true,
+            (_, Type::NominalInstance(instance)) if instance.class().is_object(db) => true,
 
             // A fully static typevar is always a subtype of itself, and is never a subtype of any
             // other typevar, since there is no guarantee that they will be specialized to the same
@@ -1160,6 +1168,22 @@ impl<'db> Type<'db> {
                 false
             }
 
+            (Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
+                let call_symbol = self.member(db, "__call__").symbol;
+                match call_symbol {
+                    Symbol::Type(Type::BoundMethod(call_function), _) => call_function
+                        .into_callable_type(db)
+                        .is_subtype_of(db, target),
+                    _ => false,
+                }
+            }
+            (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
+                left.is_subtype_of(db, right)
+            }
+            // A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`.
+            (Type::ProtocolInstance(_), _) => false,
+            (_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol),
+
             (Type::Callable(_), _) => {
                 // TODO: Implement subtyping between callable types and other types like
                 // function literals, bound methods, class literals, `type[]`, etc.)
@@ -1248,7 +1272,7 @@ impl<'db> Type<'db> {
                     metaclass_instance_type.is_subtype_of(db, target)
                 }),
 
-            // For example: `Type::KnownInstance(KnownInstanceType::Type)` is a subtype of `Type::Instance(_SpecialForm)`,
+            // For example: `Type::KnownInstance(KnownInstanceType::Type)` is a subtype of `Type::NominalInstance(_SpecialForm)`,
             // because `Type::KnownInstance(KnownInstanceType::Type)` is a set with exactly one runtime value in it
             // (the symbol `typing.Type`), and that symbol is known to be an instance of `typing._SpecialForm` at runtime.
             (Type::KnownInstance(left), right) => {
@@ -1257,20 +1281,10 @@ impl<'db> Type<'db> {
 
             // `bool` is a subtype of `int`, because `bool` subclasses `int`,
             // which means that all instances of `bool` are also instances of `int`
-            (Type::Instance(self_instance), Type::Instance(target_instance)) => {
+            (Type::NominalInstance(self_instance), Type::NominalInstance(target_instance)) => {
                 self_instance.is_subtype_of(db, target_instance)
             }
 
-            (Type::Instance(_), Type::Callable(_)) => {
-                let call_symbol = self.member(db, "__call__").symbol;
-                match call_symbol {
-                    Symbol::Type(Type::BoundMethod(call_function), _) => call_function
-                        .into_callable_type(db)
-                        .is_subtype_of(db, target),
-                    _ => false,
-                }
-            }
-
             (Type::PropertyInstance(_), _) => KnownClass::Property
                 .to_instance(db)
                 .is_subtype_of(db, target),
@@ -1280,7 +1294,7 @@ impl<'db> Type<'db> {
 
             // Other than the special cases enumerated above, `Instance` types and typevars are
             // never subtypes of any other variants
-            (Type::Instance(_) | Type::TypeVar(_), _) => false,
+            (Type::NominalInstance(_) | Type::TypeVar(_), _) => false,
         }
     }
 
@@ -1302,7 +1316,7 @@ impl<'db> Type<'db> {
 
             // All types are assignable to `object`.
             // TODO this special case might be removable once the below cases are comprehensive
-            (_, Type::Instance(instance)) if instance.class().is_object(db) => true,
+            (_, Type::NominalInstance(instance)) if instance.class().is_object(db) => true,
 
             // A typevar is always assignable to itself, and is never assignable to any other
             // typevar, since there is no guarantee that they will be specialized to the same
@@ -1436,7 +1450,7 @@ impl<'db> Type<'db> {
             // subtypes of `type[object]` are `type[...]` types (or `Never`), and `type[Any]` can
             // materialize to any `type[...]` type (or to `type[Never]`, which is equivalent to
             // `Never`.)
-            (Type::SubclassOf(subclass_of_ty), Type::Instance(_))
+            (Type::SubclassOf(subclass_of_ty), Type::NominalInstance(_))
                 if subclass_of_ty.is_dynamic()
                     && (KnownClass::Type
                         .to_instance(db)
@@ -1448,44 +1462,14 @@ impl<'db> Type<'db> {
 
             // Any type that is assignable to `type[object]` is also assignable to `type[Any]`,
             // because `type[Any]` can materialize to `type[object]`.
-            (Type::Instance(_), Type::SubclassOf(subclass_of_ty))
+            (Type::NominalInstance(_), Type::SubclassOf(subclass_of_ty))
                 if subclass_of_ty.is_dynamic()
                     && self.is_assignable_to(db, KnownClass::Type.to_instance(db)) =>
             {
                 true
             }
 
-            // TODO: This is a workaround to avoid false positives (e.g. when checking function calls
-            // with `SupportsIndex` parameters), which should be removed when we understand protocols.
-            (lhs, Type::Instance(instance))
-                if instance.class().is_known(db, KnownClass::SupportsIndex) =>
-            {
-                match lhs {
-                    Type::Instance(instance)
-                        if matches!(
-                            instance.class().known(db),
-                            Some(KnownClass::Int | KnownClass::SupportsIndex)
-                        ) =>
-                    {
-                        true
-                    }
-                    Type::IntLiteral(_) => true,
-                    _ => false,
-                }
-            }
-
-            // TODO: ditto for avoiding false positives when checking function calls with `Sized` parameters.
-            (lhs, Type::Instance(instance)) if instance.class().is_known(db, KnownClass::Sized) => {
-                matches!(
-                    lhs.to_meta_type(db).member(db, "__len__"),
-                    SymbolAndQualifiers {
-                        symbol: Symbol::Type(..),
-                        ..
-                    }
-                )
-            }
-
-            (Type::Instance(self_instance), Type::Instance(target_instance)) => {
+            (Type::NominalInstance(self_instance), Type::NominalInstance(target_instance)) => {
                 self_instance.is_assignable_to(db, target_instance)
             }
 
@@ -1493,7 +1477,7 @@ impl<'db> Type<'db> {
                 self_callable.is_assignable_to(db, target_callable)
             }
 
-            (Type::Instance(_), Type::Callable(_)) => {
+            (Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
                 let call_symbol = self.member(db, "__call__").symbol;
                 match call_symbol {
                     Symbol::Type(Type::BoundMethod(call_function), _) => call_function
@@ -1520,6 +1504,15 @@ impl<'db> Type<'db> {
                 .into_callable_type(db)
                 .is_assignable_to(db, target),
 
+            (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
+                left.is_assignable_to(db, right)
+            }
+            // Other than the dynamic types such as `Any`/`Unknown`/`Todo` handled above,
+            // a protocol instance can never be assignable to a nominal type,
+            // with the *sole* exception of `object`.
+            (Type::ProtocolInstance(_), _) => false,
+            (_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol),
+
             // TODO other types containing gradual forms
             _ => self.is_subtype_of(db, target),
         }
@@ -1540,7 +1533,16 @@ impl<'db> Type<'db> {
             }
             (Type::Tuple(left), Type::Tuple(right)) => left.is_equivalent_to(db, right),
             (Type::Callable(left), Type::Callable(right)) => left.is_equivalent_to(db, right),
-            (Type::Instance(left), Type::Instance(right)) => left.is_equivalent_to(db, right),
+            (Type::NominalInstance(left), Type::NominalInstance(right)) => {
+                left.is_equivalent_to(db, right)
+            }
+            (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
+                left.is_equivalent_to(db, right)
+            }
+            (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n))
+            | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => {
+                n.class().is_object(db) && protocol.normalized(db) == nominal
+            }
             _ => self == other && self.is_fully_static(db) && other.is_fully_static(db),
         }
     }
@@ -1575,7 +1577,7 @@ impl<'db> Type<'db> {
 
             (Type::TypeVar(first), Type::TypeVar(second)) => first == second,
 
-            (Type::Instance(first), Type::Instance(second)) => {
+            (Type::NominalInstance(first), Type::NominalInstance(second)) => {
                 first.is_gradual_equivalent_to(db, second)
             }
 
@@ -1591,6 +1593,13 @@ impl<'db> Type<'db> {
                 first.is_gradual_equivalent_to(db, second)
             }
 
+            (Type::ProtocolInstance(first), Type::ProtocolInstance(second)) => {
+                first.is_gradual_equivalent_to(db, second)
+            }
+            (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n))
+            | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => {
+                n.class().is_object(db) && protocol.normalized(db) == nominal
+            }
             _ => false,
         }
     }
@@ -1791,6 +1800,68 @@ impl<'db> Type<'db> {
                 ty.bool(db).is_always_true()
             }
 
+            (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
+                left.is_disjoint_from(db, right)
+            }
+
+            // TODO: we could also consider `protocol` to be disjoint from `nominal` if `nominal`
+            // has the right member but the type of its member is disjoint from the type of the
+            // member on `protocol`.
+            (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n))
+            | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => {
+                n.class().is_final(db) && !nominal.satisfies_protocol(db, protocol)
+            }
+
+            (
+                ty @ (Type::LiteralString
+                | Type::StringLiteral(..)
+                | Type::BytesLiteral(..)
+                | Type::BooleanLiteral(..)
+                | Type::SliceLiteral(..)
+                | Type::ClassLiteral(..)
+                | Type::FunctionLiteral(..)
+                | Type::ModuleLiteral(..)
+                | Type::GenericAlias(..)
+                | Type::IntLiteral(..)),
+                Type::ProtocolInstance(protocol),
+            )
+            | (
+                Type::ProtocolInstance(protocol),
+                ty @ (Type::LiteralString
+                | Type::StringLiteral(..)
+                | Type::BytesLiteral(..)
+                | Type::BooleanLiteral(..)
+                | Type::SliceLiteral(..)
+                | Type::ClassLiteral(..)
+                | Type::FunctionLiteral(..)
+                | Type::ModuleLiteral(..)
+                | Type::GenericAlias(..)
+                | Type::IntLiteral(..)),
+            ) => !ty.satisfies_protocol(db, protocol),
+
+            (Type::ProtocolInstance(protocol), Type::KnownInstance(known_instance))
+            | (Type::KnownInstance(known_instance), Type::ProtocolInstance(protocol)) => {
+                !known_instance
+                    .instance_fallback(db)
+                    .satisfies_protocol(db, protocol)
+            }
+
+            (Type::Callable(_), Type::ProtocolInstance(_))
+            | (Type::ProtocolInstance(_), Type::Callable(_)) => {
+                // TODO disjointness between `Callable` and `ProtocolInstance`
+                false
+            }
+
+            (Type::Tuple(..), Type::ProtocolInstance(..))
+            | (Type::ProtocolInstance(..), Type::Tuple(..)) => {
+                // Currently we do not make any general assumptions about the disjointness of a `Tuple` type
+                // and a `ProtocolInstance` type because a `Tuple` type can be an instance of a tuple
+                // subclass.
+                //
+                // TODO when we capture the types of the protocol members, we can improve on this.
+                false
+            }
+
             // for `type[Any]`/`type[Unknown]`/`type[Todo]`, we know the type cannot be any larger than `type`,
             // so although the type is dynamic we can still determine disjointedness in some situations
             (Type::SubclassOf(subclass_of_ty), other)
@@ -1803,8 +1874,8 @@ impl<'db> Type<'db> {
                     .is_disjoint_from(db, other),
             },
 
-            (Type::KnownInstance(known_instance), Type::Instance(instance))
-            | (Type::Instance(instance), Type::KnownInstance(known_instance)) => {
+            (Type::KnownInstance(known_instance), Type::NominalInstance(instance))
+            | (Type::NominalInstance(instance), Type::KnownInstance(known_instance)) => {
                 !known_instance.is_instance_of(db, instance.class())
             }
 
@@ -1813,8 +1884,8 @@ impl<'db> Type<'db> {
                 known_instance_ty.is_disjoint_from(db, KnownClass::Tuple.to_instance(db))
             }
 
-            (Type::BooleanLiteral(..), Type::Instance(instance))
-            | (Type::Instance(instance), Type::BooleanLiteral(..)) => {
+            (Type::BooleanLiteral(..), Type::NominalInstance(instance))
+            | (Type::NominalInstance(instance), Type::BooleanLiteral(..)) => {
                 // A `Type::BooleanLiteral()` must be an instance of exactly `bool`
                 // (it cannot be an instance of a `bool` subclass)
                 !KnownClass::Bool.is_subclass_of(db, instance.class())
@@ -1822,8 +1893,8 @@ impl<'db> Type<'db> {
 
             (Type::BooleanLiteral(..), _) | (_, Type::BooleanLiteral(..)) => true,
 
-            (Type::IntLiteral(..), Type::Instance(instance))
-            | (Type::Instance(instance), Type::IntLiteral(..)) => {
+            (Type::IntLiteral(..), Type::NominalInstance(instance))
+            | (Type::NominalInstance(instance), Type::IntLiteral(..)) => {
                 // A `Type::IntLiteral()` must be an instance of exactly `int`
                 // (it cannot be an instance of an `int` subclass)
                 !KnownClass::Int.is_subclass_of(db, instance.class())
@@ -1834,8 +1905,8 @@ impl<'db> Type<'db> {
             (Type::StringLiteral(..), Type::LiteralString)
             | (Type::LiteralString, Type::StringLiteral(..)) => false,
 
-            (Type::StringLiteral(..) | Type::LiteralString, Type::Instance(instance))
-            | (Type::Instance(instance), Type::StringLiteral(..) | Type::LiteralString) => {
+            (Type::StringLiteral(..) | Type::LiteralString, Type::NominalInstance(instance))
+            | (Type::NominalInstance(instance), Type::StringLiteral(..) | Type::LiteralString) => {
                 // A `Type::StringLiteral()` or a `Type::LiteralString` must be an instance of exactly `str`
                 // (it cannot be an instance of a `str` subclass)
                 !KnownClass::Str.is_subclass_of(db, instance.class())
@@ -1844,15 +1915,15 @@ impl<'db> Type<'db> {
             (Type::LiteralString, Type::LiteralString) => false,
             (Type::LiteralString, _) | (_, Type::LiteralString) => true,
 
-            (Type::BytesLiteral(..), Type::Instance(instance))
-            | (Type::Instance(instance), Type::BytesLiteral(..)) => {
+            (Type::BytesLiteral(..), Type::NominalInstance(instance))
+            | (Type::NominalInstance(instance), Type::BytesLiteral(..)) => {
                 // A `Type::BytesLiteral()` must be an instance of exactly `bytes`
                 // (it cannot be an instance of a `bytes` subclass)
                 !KnownClass::Bytes.is_subclass_of(db, instance.class())
             }
 
-            (Type::SliceLiteral(..), Type::Instance(instance))
-            | (Type::Instance(instance), Type::SliceLiteral(..)) => {
+            (Type::SliceLiteral(..), Type::NominalInstance(instance))
+            | (Type::NominalInstance(instance), Type::SliceLiteral(..)) => {
                 // A `Type::SliceLiteral` must be an instance of exactly `slice`
                 // (it cannot be an instance of a `slice` subclass)
                 !KnownClass::Slice.is_subclass_of(db, instance.class())
@@ -1861,17 +1932,19 @@ impl<'db> Type<'db> {
             // A class-literal type `X` is always disjoint from an instance type `Y`,
             // unless the type expressing "all instances of `Z`" is a subtype of of `Y`,
             // where `Z` is `X`'s metaclass.
-            (Type::ClassLiteral(class), instance @ Type::Instance(_))
-            | (instance @ Type::Instance(_), Type::ClassLiteral(class)) => !class
-                .metaclass_instance_type(db)
-                .is_subtype_of(db, instance),
-            (Type::GenericAlias(alias), instance @ Type::Instance(_))
-            | (instance @ Type::Instance(_), Type::GenericAlias(alias)) => !ClassType::from(alias)
+            (Type::ClassLiteral(class), instance @ Type::NominalInstance(_))
+            | (instance @ Type::NominalInstance(_), Type::ClassLiteral(class)) => !class
                 .metaclass_instance_type(db)
                 .is_subtype_of(db, instance),
+            (Type::GenericAlias(alias), instance @ Type::NominalInstance(_))
+            | (instance @ Type::NominalInstance(_), Type::GenericAlias(alias)) => {
+                !ClassType::from(alias)
+                    .metaclass_instance_type(db)
+                    .is_subtype_of(db, instance)
+            }
 
-            (Type::FunctionLiteral(..), Type::Instance(instance))
-            | (Type::Instance(instance), Type::FunctionLiteral(..)) => {
+            (Type::FunctionLiteral(..), Type::NominalInstance(instance))
+            | (Type::NominalInstance(instance), Type::FunctionLiteral(..)) => {
                 // A `Type::FunctionLiteral()` must be an instance of exactly `types.FunctionType`
                 // (it cannot be an instance of a `types.FunctionType` subclass)
                 !KnownClass::FunctionType.is_subclass_of(db, instance.class())
@@ -1927,13 +2000,15 @@ impl<'db> Type<'db> {
                 false
             }
 
-            (Type::ModuleLiteral(..), other @ Type::Instance(..))
-            | (other @ Type::Instance(..), Type::ModuleLiteral(..)) => {
+            (Type::ModuleLiteral(..), other @ Type::NominalInstance(..))
+            | (other @ Type::NominalInstance(..), Type::ModuleLiteral(..)) => {
                 // Modules *can* actually be instances of `ModuleType` subclasses
                 other.is_disjoint_from(db, KnownClass::ModuleType.to_instance(db))
             }
 
-            (Type::Instance(left), Type::Instance(right)) => left.is_disjoint_from(db, right),
+            (Type::NominalInstance(left), Type::NominalInstance(right)) => {
+                left.is_disjoint_from(db, right)
+            }
 
             (Type::Tuple(tuple), Type::Tuple(other_tuple)) => {
                 let self_elements = tuple.elements(db);
@@ -1945,8 +2020,8 @@ impl<'db> Type<'db> {
                         .any(|(e1, e2)| e1.is_disjoint_from(db, *e2))
             }
 
-            (Type::Tuple(..), instance @ Type::Instance(_))
-            | (instance @ Type::Instance(_), Type::Tuple(..)) => {
+            (Type::Tuple(..), instance @ Type::NominalInstance(_))
+            | (instance @ Type::NominalInstance(_), Type::Tuple(..)) => {
                 // We cannot be sure if the tuple is disjoint from the instance because:
                 //   - 'other' might be the homogeneous arbitrary-length tuple type
                 //     tuple[T, ...] (which we don't have support for yet); if all of
@@ -1992,6 +2067,8 @@ impl<'db> Type<'db> {
             | Type::AlwaysTruthy
             | Type::PropertyInstance(_) => true,
 
+            Type::ProtocolInstance(protocol) => protocol.is_fully_static(),
+
             Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
                 None => true,
                 Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.is_fully_static(db),
@@ -2006,7 +2083,7 @@ impl<'db> Type<'db> {
                 !matches!(bound_super.pivot_class(db), ClassBase::Dynamic(_))
                     && !matches!(bound_super.owner(db), SuperOwnerKind::Dynamic(_))
             }
-            Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::Instance(_) => {
+            Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::NominalInstance(_) => {
                 // TODO: Ideally, we would iterate over the MRO of the class, check if all
                 // bases are fully static, and only return `true` if that is the case.
                 //
@@ -2057,6 +2134,26 @@ impl<'db> Type<'db> {
                 false
             }
 
+            Type::ProtocolInstance(..) => {
+                // It *might* be possible to have a singleton protocol-instance type...?
+                //
+                // E.g.:
+                //
+                // ```py
+                // from typing import Protocol, Callable
+                //
+                // class WeirdAndWacky(Protocol):
+                //     @property
+                //     def __class__(self) -> Callable[[], None]: ...
+                // ```
+                //
+                // `WeirdAndWacky` only has a single possible inhabitant: `None`!
+                // It is thus a singleton type.
+                // However, going out of our way to recognise it as such is probably not worth it.
+                // Such cases should anyway be exceedingly rare and/or contrived.
+                false
+            }
+
             // An unbounded, unconstrained typevar is not a singleton, because it can be
             // specialized to a non-singleton type. A bounded typevar is not a singleton, even if
             // the bound is a final singleton class, since it can still be specialized to `Never`.
@@ -2112,7 +2209,7 @@ impl<'db> Type<'db> {
                 false
             }
             Type::DataclassDecorator(_) | Type::DataclassTransformer(_) => false,
-            Type::Instance(instance) => instance.is_singleton(db),
+            Type::NominalInstance(instance) => instance.is_singleton(db),
             Type::PropertyInstance(_) => false,
             Type::Tuple(..) => {
                 // The empty tuple is a singleton on CPython and PyPy, but not on other Python
@@ -2159,6 +2256,11 @@ impl<'db> Type<'db> {
             | Type::SliceLiteral(..)
             | Type::KnownInstance(..) => true,
 
+            Type::ProtocolInstance(..) => {
+                // See comment in the `Type::ProtocolInstance` branch for `Type::is_singleton`.
+                false
+            }
+
             // An unbounded, unconstrained typevar is not single-valued, because it can be
             // specialized to a multiple-valued type. A bounded typevar is not single-valued, even
             // if the bound is a final single-valued class, since it can still be specialized to
@@ -2184,7 +2286,7 @@ impl<'db> Type<'db> {
                 .iter()
                 .all(|elem| elem.is_single_valued(db)),
 
-            Type::Instance(instance) => instance.is_single_valued(db),
+            Type::NominalInstance(instance) => instance.is_single_valued(db),
 
             Type::BoundSuper(_) => {
                 // At runtime two super instances never compare equal, even if their arguments are identical.
@@ -2322,10 +2424,11 @@ impl<'db> Type<'db> {
                 .to_class_literal(db)
                 .find_name_in_mro_with_policy(db, name, policy),
 
-            // We eagerly normalize type[object], i.e. Type::SubclassOf(object) to `type`, i.e. Type::Instance(type).
-            // So looking up a name in the MRO of `Type::Instance(type)` is equivalent to looking up the name in the
+            // We eagerly normalize type[object], i.e. Type::SubclassOf(object) to `type`,
+            // i.e. Type::NominalInstance(type). So looking up a name in the MRO of
+            // `Type::NominalInstance(type)` is equivalent to looking up the name in the
             // MRO of the class `object`.
-            Type::Instance(instance) if instance.class().is_known(db, KnownClass::Type) => {
+            Type::NominalInstance(instance) if instance.class().is_known(db, KnownClass::Type) => {
                 KnownClass::Object
                     .to_class_literal(db)
                     .find_name_in_mro_with_policy(db, name, policy)
@@ -2350,7 +2453,8 @@ impl<'db> Type<'db> {
             | Type::SliceLiteral(_)
             | Type::Tuple(_)
             | Type::TypeVar(_)
-            | Type::Instance(_)
+            | Type::NominalInstance(_)
+            | Type::ProtocolInstance(_)
             | Type::PropertyInstance(_) => None,
         }
     }
@@ -2415,7 +2519,18 @@ impl<'db> Type<'db> {
 
             Type::Dynamic(_) | Type::Never => Symbol::bound(self).into(),
 
-            Type::Instance(instance) => instance.class().instance_member(db, name),
+            Type::NominalInstance(instance) => instance.class().instance_member(db, name),
+
+            Type::ProtocolInstance(protocol) => match protocol.inner() {
+                Protocol::FromClass(class) => class.instance_member(db, name),
+                Protocol::Synthesized(synthesized) => {
+                    if synthesized.members(db).contains(name) {
+                        SymbolAndQualifiers::todo("Capture type of synthesized protocol members")
+                    } else {
+                        Symbol::Unbound.into()
+                    }
+                }
+            },
 
             Type::FunctionLiteral(_) => KnownClass::FunctionType
                 .to_instance(db)
@@ -2856,7 +2971,7 @@ impl<'db> Type<'db> {
                 .to_instance(db)
                 .member_lookup_with_policy(db, name, policy),
 
-            Type::Instance(instance)
+            Type::NominalInstance(instance)
                 if matches!(name.as_str(), "major" | "minor")
                     && instance.class().is_known(db, KnownClass::VersionInfo) =>
             {
@@ -2898,7 +3013,8 @@ impl<'db> Type<'db> {
                 policy,
             ),
 
-            Type::Instance(..)
+            Type::NominalInstance(..)
+            | Type::ProtocolInstance(..)
             | Type::BooleanLiteral(..)
             | Type::IntLiteral(..)
             | Type::StringLiteral(..)
@@ -2929,7 +3045,7 @@ impl<'db> Type<'db> {
                     // It will need a special handling, so it remember the origin type to properly
                     // resolve the attribute.
                     if matches!(
-                        self.into_instance()
+                        self.into_nominal_instance()
                             .and_then(|instance| instance.class().known(db)),
                         Some(KnownClass::ModuleType | KnownClass::GenericAlias)
                     ) {
@@ -3190,11 +3306,13 @@ impl<'db> Type<'db> {
                 }
             },
 
-            Type::Instance(instance) => match instance.class().known(db) {
+            Type::NominalInstance(instance) => match instance.class().known(db) {
                 Some(known_class) => known_class.bool(),
                 None => try_dunder_bool()?,
             },
 
+            Type::ProtocolInstance(_) => try_dunder_bool()?,
+
             Type::KnownInstance(known_instance) => known_instance.bool(),
 
             Type::PropertyInstance(_) => Truthiness::AlwaysTrue,
@@ -4006,7 +4124,7 @@ impl<'db> Type<'db> {
                 SubclassOfInner::Class(class) => Type::from(class).signatures(db),
             },
 
-            Type::Instance(_) => {
+            Type::NominalInstance(_) | Type::ProtocolInstance(_) => {
                 // Note that for objects that have a (possibly not callable!) `__call__` attribute,
                 // we will get the signature of the `__call__` attribute, but will pass in the type
                 // of the original object as the "callable type". That ensures that we get errors
@@ -4419,11 +4537,14 @@ impl<'db> Type<'db> {
                 };
                 let specialized = specialization
                     .map(|specialization| {
-                        Type::instance(ClassType::Generic(GenericAlias::new(
+                        Type::instance(
                             db,
-                            generic_origin,
-                            specialization,
-                        )))
+                            ClassType::Generic(GenericAlias::new(
+                                db,
+                                generic_origin,
+                                specialization,
+                            )),
+                        )
                     })
                     .unwrap_or(instance_ty);
                 Ok(specialized)
@@ -4454,9 +4575,9 @@ impl<'db> Type<'db> {
     pub fn to_instance(&self, db: &'db dyn Db) -> Option<Type<'db>> {
         match self {
             Type::Dynamic(_) | Type::Never => Some(*self),
-            Type::ClassLiteral(class) => Some(Type::instance(class.default_specialization(db))),
-            Type::GenericAlias(alias) => Some(Type::instance(ClassType::from(*alias))),
-            Type::SubclassOf(subclass_of_ty) => Some(subclass_of_ty.to_instance()),
+            Type::ClassLiteral(class) => Some(Type::instance(db, class.default_specialization(db))),
+            Type::GenericAlias(alias) => Some(Type::instance(db, ClassType::from(*alias))),
+            Type::SubclassOf(subclass_of_ty) => Some(subclass_of_ty.to_instance(db)),
             Type::Union(union) => {
                 let mut builder = UnionBuilder::new(db);
                 for element in union.elements(db) {
@@ -4474,7 +4595,8 @@ impl<'db> Type<'db> {
             | Type::WrapperDescriptor(_)
             | Type::DataclassDecorator(_)
             | Type::DataclassTransformer(_)
-            | Type::Instance(_)
+            | Type::NominalInstance(_)
+            | Type::ProtocolInstance(_)
             | Type::KnownInstance(_)
             | Type::PropertyInstance(_)
             | Type::ModuleLiteral(_)
@@ -4494,7 +4616,7 @@ impl<'db> Type<'db> {
     ///
     /// For example, the builtin `int` as a value expression is of type
     /// `Type::ClassLiteral(builtins.int)`, that is, it is the `int` class itself. As a type
-    /// expression, it names the type `Type::Instance(builtins.int)`, that is, all objects whose
+    /// expression, it names the type `Type::NominalInstance(builtins.int)`, that is, all objects whose
     /// `__class__` is `int`.
     pub fn in_type_expression(
         &self,
@@ -4521,11 +4643,11 @@ impl<'db> Type<'db> {
                             KnownClass::Float.to_instance(db),
                         ],
                     ),
-                    _ => Type::instance(class.default_specialization(db)),
+                    _ => Type::instance(db, class.default_specialization(db)),
                 };
                 Ok(ty)
             }
-            Type::GenericAlias(alias) => Ok(Type::instance(ClassType::from(*alias))),
+            Type::GenericAlias(alias) => Ok(Type::instance(db, ClassType::from(*alias))),
 
             Type::SubclassOf(_)
             | Type::BooleanLiteral(_)
@@ -4548,6 +4670,7 @@ impl<'db> Type<'db> {
             | Type::Never
             | Type::FunctionLiteral(_)
             | Type::BoundSuper(_)
+            | Type::ProtocolInstance(_)
             | Type::PropertyInstance(_) => Err(InvalidTypeExpressionError {
                 invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType(*self)],
                 fallback_type: Type::unknown(),
@@ -4672,7 +4795,7 @@ impl<'db> Type<'db> {
 
             Type::Dynamic(_) => Ok(*self),
 
-            Type::Instance(instance) => match instance.class().known(db) {
+            Type::NominalInstance(instance) => match instance.class().known(db) {
                 Some(KnownClass::TypeVar) => Ok(todo_type!(
                     "Support for `typing.TypeVar` instances in type expressions"
                 )),
@@ -4748,7 +4871,7 @@ impl<'db> Type<'db> {
     pub fn to_meta_type(&self, db: &'db dyn Db) -> Type<'db> {
         match self {
             Type::Never => Type::Never,
-            Type::Instance(instance) => instance.to_meta_type(db),
+            Type::NominalInstance(instance) => instance.to_meta_type(db),
             Type::KnownInstance(known_instance) => known_instance.to_meta_type(db),
             Type::PropertyInstance(_) => KnownClass::Property.to_class_literal(db),
             Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)),
@@ -4796,6 +4919,7 @@ impl<'db> Type<'db> {
             ),
             Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db),
             Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db),
+            Type::ProtocolInstance(protocol) => protocol.to_meta_type(db),
         }
     }
 
@@ -4917,9 +5041,11 @@ impl<'db> Type<'db> {
             | Type::BytesLiteral(_)
             | Type::SliceLiteral(_)
             | Type::BoundSuper(_)
-            // Instance contains a ClassType, which has already been specialized if needed, like
-            // above with BoundMethod's self_instance.
-            | Type::Instance(_)
+            // `NominalInstance` contains a ClassType, which has already been specialized if needed,
+            // like above with BoundMethod's self_instance.
+            | Type::NominalInstance(_)
+            // Same for `ProtocolInstance`
+            | Type::ProtocolInstance(_)
             | Type::KnownInstance(_) => self,
         }
     }
@@ -5006,7 +5132,8 @@ impl<'db> Type<'db> {
             | Type::BytesLiteral(_)
             | Type::SliceLiteral(_)
             | Type::BoundSuper(_)
-            | Type::Instance(_)
+            | Type::NominalInstance(_)
+            | Type::ProtocolInstance(_)
             | Type::KnownInstance(_) => {}
         }
     }
@@ -5066,7 +5193,7 @@ impl<'db> Type<'db> {
                 Some(TypeDefinition::Class(class_literal.definition(db)))
             }
             Self::GenericAlias(alias) => Some(TypeDefinition::Class(alias.definition(db))),
-            Self::Instance(instance) => {
+            Self::NominalInstance(instance) => {
                 Some(TypeDefinition::Class(instance.class().definition(db)))
             }
             Self::KnownInstance(instance) => match instance {
@@ -5100,6 +5227,11 @@ impl<'db> Type<'db> {
 
             Self::TypeVar(var) => Some(TypeDefinition::TypeVar(var.definition(db))),
 
+            Self::ProtocolInstance(protocol) => match protocol.inner() {
+                Protocol::FromClass(class) => Some(TypeDefinition::Class(class.definition(db))),
+                Protocol::Synthesized(_) => None,
+            },
+
             Self::Union(_) | Self::Intersection(_) => None,
 
             // These types have no definition
@@ -7761,7 +7893,7 @@ impl BoundSuperError<'_> {
 pub enum SuperOwnerKind<'db> {
     Dynamic(DynamicType),
     Class(ClassType<'db>),
-    Instance(InstanceType<'db>),
+    Instance(NominalInstanceType<'db>),
 }
 
 impl<'db> SuperOwnerKind<'db> {
@@ -7795,7 +7927,7 @@ impl<'db> SuperOwnerKind<'db> {
             Type::ClassLiteral(class_literal) => Some(SuperOwnerKind::Class(
                 class_literal.apply_optional_specialization(db, None),
             )),
-            Type::Instance(instance) => Some(SuperOwnerKind::Instance(instance)),
+            Type::NominalInstance(instance) => Some(SuperOwnerKind::Instance(instance)),
             Type::BooleanLiteral(_) => {
                 SuperOwnerKind::try_from_type(db, KnownClass::Bool.to_instance(db))
             }
diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs
index 8f81db8d3f6dc1..3efbd2db0a93d4 100644
--- a/crates/red_knot_python_semantic/src/types/builder.rs
+++ b/crates/red_knot_python_semantic/src/types/builder.rs
@@ -591,7 +591,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
             }
             _ => {
                 let known_instance = new_positive
-                    .into_instance()
+                    .into_nominal_instance()
                     .and_then(|instance| instance.class().known(db));
 
                 if known_instance == Some(KnownClass::Object) {
@@ -611,7 +611,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
                         Type::AlwaysFalsy if addition_is_bool_instance => {
                             new_positive = Type::BooleanLiteral(false);
                         }
-                        Type::Instance(instance)
+                        Type::NominalInstance(instance)
                             if instance.class().is_known(db, KnownClass::Bool) =>
                         {
                             match new_positive {
@@ -705,7 +705,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
         let contains_bool = || {
             self.positive
                 .iter()
-                .filter_map(|ty| ty.into_instance())
+                .filter_map(|ty| ty.into_nominal_instance())
                 .filter_map(|instance| instance.class().known(db))
                 .any(KnownClass::is_bool)
         };
@@ -722,7 +722,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
             Type::Never => {
                 // Adding ~Never to an intersection is a no-op.
             }
-            Type::Instance(instance) if instance.class().is_object(db) => {
+            Type::NominalInstance(instance) if instance.class().is_object(db) => {
                 // Adding ~object to an intersection results in Never.
                 *self = Self::default();
                 self.positive.insert(Type::Never);
diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs
index 0a4870447550ed..a2ccc66e8d3e5a 100644
--- a/crates/red_knot_python_semantic/src/types/class.rs
+++ b/crates/red_knot_python_semantic/src/types/class.rs
@@ -533,14 +533,33 @@ impl<'db> ClassLiteral<'db> {
     }
 
     /// Determine if this class is a protocol.
+    ///
+    /// This method relies on the accuracy of the [`KnownClass::is_protocol`] method,
+    /// which hardcodes knowledge about certain special-cased classes. See the docs on
+    /// that method for why we do this rather than relying on generalised logic for all
+    /// classes, including the special-cased ones that are included in the [`KnownClass`]
+    /// enum.
     pub(super) fn is_protocol(self, db: &'db dyn Db) -> bool {
-        self.explicit_bases(db).iter().rev().take(3).any(|base| {
-            matches!(
-                base,
-                Type::KnownInstance(KnownInstanceType::Protocol)
-                    | Type::Dynamic(DynamicType::SubscriptedProtocol)
-            )
-        })
+        self.known(db)
+            .map(KnownClass::is_protocol)
+            .unwrap_or_else(|| {
+                // Iterate through the last three bases of the class
+                // searching for `Protocol` or `Protocol[]` in the bases list.
+                //
+                // If `Protocol` is present in the bases list of a valid protocol class, it must either:
+                //
+                // - be the last base
+                // - OR be the last-but-one base (with the final base being `Generic[]` or `object`)
+                // - OR be the last-but-two base (with the penultimate base being `Generic[]`
+                //                                and the final base being `object`)
+                self.explicit_bases(db).iter().rev().take(3).any(|base| {
+                    matches!(
+                        base,
+                        Type::KnownInstance(KnownInstanceType::Protocol)
+                            | Type::Dynamic(DynamicType::SubscriptedProtocol)
+                    )
+                })
+            })
     }
 
     /// Return the types of the decorators on this class
@@ -1076,6 +1095,7 @@ impl<'db> ClassLiteral<'db> {
                     Parameters::new([Parameter::positional_or_keyword(Name::new_static("other"))
                         // TODO: could be `Self`.
                         .with_annotated_type(Type::instance(
+                            db,
                             self.apply_optional_specialization(db, specialization),
                         ))]),
                     Some(KnownClass::Bool.to_instance(db)),
@@ -1711,7 +1731,7 @@ impl<'db> ProtocolClassLiteral<'db> {
     /// It is illegal for a protocol class to have any instance attributes that are not declared
     /// in the protocol's class body. If any are assigned to, they are not taken into account in
     /// the protocol's list of members.
-    pub(super) fn protocol_members(self, db: &'db dyn Db) -> &'db ordermap::set::Slice<Name> {
+    pub(super) fn protocol_members(self, db: &'db dyn Db) -> &'db FxOrderSet<Name> {
         /// The list of excluded members is subject to change between Python versions,
         /// especially for dunders, but it probably doesn't matter *too* much if this
         /// list goes out of date. It's up to date as of Python commit 87b1ea016b1454b1e83b9113fa9435849b7743aa
@@ -1748,11 +1768,11 @@ impl<'db> ProtocolClassLiteral<'db> {
             )
         }
 
-        #[salsa::tracked(return_ref)]
+        #[salsa::tracked(return_ref, cycle_fn=proto_members_cycle_recover, cycle_initial=proto_members_cycle_initial)]
         fn cached_protocol_members<'db>(
             db: &'db dyn Db,
             class: ClassLiteral<'db>,
-        ) -> Box<ordermap::set::Slice<Name>> {
+        ) -> FxOrderSet<Name> {
             let mut members = FxOrderSet::default();
 
             for parent_protocol in class
@@ -1796,9 +1816,24 @@ impl<'db> ProtocolClassLiteral<'db> {
             }
 
             members.sort();
-            members.into_boxed_slice()
+            members.shrink_to_fit();
+            members
         }
 
+        fn proto_members_cycle_recover(
+            _db: &dyn Db,
+            _value: &FxOrderSet<Name>,
+            _count: u32,
+            _class: ClassLiteral,
+        ) -> salsa::CycleRecoveryAction<FxOrderSet<Name>> {
+            salsa::CycleRecoveryAction::Iterate
+        }
+
+        fn proto_members_cycle_initial(_db: &dyn Db, _class: ClassLiteral) -> FxOrderSet<Name> {
+            FxOrderSet::default()
+        }
+
+        let _span = tracing::trace_span!("protocol_members", "class='{}'", self.name(db)).entered();
         cached_protocol_members(db, *self)
     }
 
@@ -1892,8 +1927,6 @@ pub enum KnownClass {
     TypeAliasType,
     NoDefaultType,
     NewType,
-    Sized,
-    // TODO: This can probably be removed when we have support for protocols
     SupportsIndex,
     // Collections
     ChainMap,
@@ -1977,7 +2010,6 @@ impl<'db> KnownClass {
             | Self::DefaultDict
             | Self::Deque
             | Self::Float
-            | Self::Sized
             | Self::Enum
             | Self::ABCMeta
             // Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9
@@ -1988,6 +2020,75 @@ impl<'db> KnownClass {
         }
     }
 
+    /// Return `true` if this class is a protocol class.
+    ///
+    /// In an ideal world, perhaps we wouldn't hardcode this knowledge here;
+    /// instead, we'd just look at the bases for these classes, as we do for
+    /// all other classes. However, the special casing here helps us out in
+    /// two important ways:
+    ///
+    /// 1. It helps us avoid Salsa cycles when creating types such as "instance of `str`"
+    ///    and "instance of `sys._version_info`". These types are constructed very early
+    ///    on, but it causes problems if we attempt to infer the types of their bases
+    ///    too soon.
+    /// 2. It's probably more performant.
+    const fn is_protocol(self) -> bool {
+        match self {
+            Self::SupportsIndex => true,
+
+            Self::Any
+            | Self::Bool
+            | Self::Object
+            | Self::Bytes
+            | Self::Bytearray
+            | Self::Tuple
+            | Self::Int
+            | Self::Float
+            | Self::Complex
+            | Self::FrozenSet
+            | Self::Str
+            | Self::Set
+            | Self::Dict
+            | Self::List
+            | Self::Type
+            | Self::Slice
+            | Self::Range
+            | Self::Property
+            | Self::BaseException
+            | Self::BaseExceptionGroup
+            | Self::Classmethod
+            | Self::GenericAlias
+            | Self::ModuleType
+            | Self::FunctionType
+            | Self::MethodType
+            | Self::MethodWrapperType
+            | Self::WrapperDescriptorType
+            | Self::NoneType
+            | Self::SpecialForm
+            | Self::TypeVar
+            | Self::ParamSpec
+            | Self::ParamSpecArgs
+            | Self::ParamSpecKwargs
+            | Self::TypeVarTuple
+            | Self::TypeAliasType
+            | Self::NoDefaultType
+            | Self::NewType
+            | Self::ChainMap
+            | Self::Counter
+            | Self::DefaultDict
+            | Self::Deque
+            | Self::OrderedDict
+            | Self::Enum
+            | Self::ABCMeta
+            | Self::Super
+            | Self::StdlibAlias
+            | Self::VersionInfo
+            | Self::EllipsisType
+            | Self::NotImplementedType
+            | Self::UnionType => false,
+        }
+    }
+
     pub(crate) fn name(self, db: &'db dyn Db) -> &'static str {
         match self {
             Self::Any => "Any",
@@ -2033,7 +2134,6 @@ impl<'db> KnownClass {
             Self::Counter => "Counter",
             Self::DefaultDict => "defaultdict",
             Self::Deque => "deque",
-            Self::Sized => "Sized",
             Self::OrderedDict => "OrderedDict",
             Self::Enum => "Enum",
             Self::ABCMeta => "ABCMeta",
@@ -2090,7 +2190,7 @@ impl<'db> KnownClass {
     pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> {
         self.to_class_literal(db)
             .to_class_type(db)
-            .map(Type::instance)
+            .map(|class| Type::instance(db, class))
             .unwrap_or_else(Type::unknown)
     }
 
@@ -2207,8 +2307,7 @@ impl<'db> KnownClass {
             | Self::SpecialForm
             | Self::TypeVar
             | Self::StdlibAlias
-            | Self::SupportsIndex
-            | Self::Sized => KnownModule::Typing,
+            | Self::SupportsIndex => KnownModule::Typing,
             Self::TypeAliasType
             | Self::TypeVarTuple
             | Self::ParamSpec
@@ -2296,7 +2395,6 @@ impl<'db> KnownClass {
             | Self::ParamSpecArgs
             | Self::ParamSpecKwargs
             | Self::TypeVarTuple
-            | Self::Sized
             | Self::Enum
             | Self::ABCMeta
             | Self::Super
@@ -2356,7 +2454,6 @@ impl<'db> KnownClass {
             | Self::ParamSpecArgs
             | Self::ParamSpecKwargs
             | Self::TypeVarTuple
-            | Self::Sized
             | Self::Enum
             | Self::ABCMeta
             | Self::Super
@@ -2418,7 +2515,6 @@ impl<'db> KnownClass {
             "_SpecialForm" => Self::SpecialForm,
             "_NoDefaultType" => Self::NoDefaultType,
             "SupportsIndex" => Self::SupportsIndex,
-            "Sized" => Self::Sized,
             "Enum" => Self::Enum,
             "ABCMeta" => Self::ABCMeta,
             "super" => Self::Super,
@@ -2491,7 +2587,6 @@ impl<'db> KnownClass {
             | Self::ParamSpecArgs
             | Self::ParamSpecKwargs
             | Self::TypeVarTuple
-            | Self::Sized
             | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
         }
     }
diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs
index 9f480ed07eaf05..50ab0a9d78c2a5 100644
--- a/crates/red_knot_python_semantic/src/types/class_base.rs
+++ b/crates/red_knot_python_semantic/src/types/class_base.rs
@@ -78,12 +78,14 @@ impl<'db> ClassBase<'db> {
                 Self::Class(literal.default_specialization(db))
             }),
             Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))),
-            Type::Instance(instance) if instance.class().is_known(db, KnownClass::GenericAlias) => {
+            Type::NominalInstance(instance)
+                if instance.class().is_known(db, KnownClass::GenericAlias) =>
+            {
                 Self::try_from_type(db, todo_type!("GenericAlias instance"))
             }
             Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
             Type::Intersection(_) => None, // TODO -- probably incorrect?
-            Type::Instance(_) => None, // TODO -- handle `__mro_entries__`?
+            Type::NominalInstance(_) => None, // TODO -- handle `__mro_entries__`?
             Type::PropertyInstance(_) => None,
             Type::Never
             | Type::BooleanLiteral(_)
@@ -104,6 +106,7 @@ impl<'db> ClassBase<'db> {
             | Type::SubclassOf(_)
             | Type::TypeVar(_)
             | Type::BoundSuper(_)
+            | Type::ProtocolInstance(_)
             | Type::AlwaysFalsy
             | Type::AlwaysTruthy => None,
             Type::KnownInstance(known_instance) => match known_instance {
diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs
index 2633938ae9285f..c878a81539fc30 100644
--- a/crates/red_knot_python_semantic/src/types/display.rs
+++ b/crates/red_knot_python_semantic/src/types/display.rs
@@ -10,15 +10,13 @@ use crate::types::class::{ClassLiteral, ClassType, GenericAlias};
 use crate::types::generics::{GenericContext, Specialization};
 use crate::types::signatures::{Parameter, Parameters, Signature};
 use crate::types::{
-    FunctionSignature, IntersectionType, KnownClass, MethodWrapperKind, StringLiteralType,
-    SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance, UnionType,
-    WrapperDescriptorKind,
+    CallableType, FunctionSignature, IntersectionType, KnownClass, MethodWrapperKind, Protocol,
+    StringLiteralType, SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance,
+    UnionType, WrapperDescriptorKind,
 };
 use crate::Db;
 use rustc_hash::FxHashMap;
 
-use super::CallableType;
-
 impl<'db> Type<'db> {
     pub fn display(&self, db: &'db dyn Db) -> DisplayType {
         DisplayType { ty: self, db }
@@ -73,11 +71,32 @@ impl Display for DisplayRepresentation<'_> {
         match self.ty {
             Type::Dynamic(dynamic) => dynamic.fmt(f),
             Type::Never => f.write_str("Never"),
-            Type::Instance(instance) => match (instance.class(), instance.class().known(self.db)) {
-                (_, Some(KnownClass::NoneType)) => f.write_str("None"),
-                (_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"),
-                (ClassType::NonGeneric(class), _) => f.write_str(class.name(self.db)),
-                (ClassType::Generic(alias), _) => write!(f, "{}", alias.display(self.db)),
+            Type::NominalInstance(instance) => {
+                match (instance.class(), instance.class().known(self.db)) {
+                    (_, Some(KnownClass::NoneType)) => f.write_str("None"),
+                    (_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"),
+                    (ClassType::NonGeneric(class), _) => f.write_str(class.name(self.db)),
+                    (ClassType::Generic(alias), _) => write!(f, "{}", alias.display(self.db)),
+                }
+            }
+            Type::ProtocolInstance(protocol) => match protocol.inner() {
+                Protocol::FromClass(ClassType::NonGeneric(class)) => {
+                    f.write_str(class.name(self.db))
+                }
+                Protocol::FromClass(ClassType::Generic(alias)) => alias.display(self.db).fmt(f),
+                Protocol::Synthesized(synthetic) => {
+                    f.write_str("<Protocol with members ")?;
+                    let member_list = synthetic.members(self.db);
+                    let num_members = member_list.len();
+                    for (i, member) in member_list.iter().enumerate() {
+                        let is_last = i == num_members - 1;
+                        write!(f, "'{member}'")?;
+                        if !is_last {
+                            f.write_str(", ")?;
+                        }
+                    }
+                    f.write_char('>')
+                }
             },
             Type::PropertyInstance(_) => f.write_str("property"),
             Type::ModuleLiteral(module) => {
@@ -761,6 +780,7 @@ mod tests {
     use ruff_python_ast::name::Name;
 
     use crate::db::tests::setup_db;
+    use crate::symbol::typing_extensions_symbol;
     use crate::types::{
         KnownClass, Parameter, Parameters, Signature, SliceLiteralType, StringLiteralType, Type,
     };
@@ -832,6 +852,31 @@ mod tests {
         );
     }
 
+    #[test]
+    fn synthesized_protocol_display() {
+        let db = setup_db();
+
+        // Call `.normalized()` to turn the class-based protocol into a nameless synthesized one.
+        let supports_index_synthesized = KnownClass::SupportsIndex.to_instance(&db).normalized(&db);
+        assert_eq!(
+            supports_index_synthesized.display(&db).to_string(),
+            "<Protocol with members '__index__'>"
+        );
+
+        let iterator_synthesized = typing_extensions_symbol(&db, "Iterator")
+            .symbol
+            .ignore_possibly_unbound()
+            .unwrap()
+            .to_instance(&db)
+            .unwrap()
+            .normalized(&db); // Call `.normalized()` to turn the class-based protocol into a nameless synthesized one.
+
+        assert_eq!(
+            iterator_synthesized.display(&db).to_string(),
+            "<Protocol with members '__iter__', '__next__'>"
+        );
+    }
+
     fn display_signature<'db>(
         db: &dyn Db,
         parameters: impl IntoIterator<Item = Parameter<'db>>,
diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs
index 6016308878674b..2e275e99924bde 100644
--- a/crates/red_knot_python_semantic/src/types/infer.rs
+++ b/crates/red_knot_python_semantic/src/types/infer.rs
@@ -1065,7 +1065,7 @@ impl<'db> TypeInferenceBuilder<'db> {
     ) -> bool {
         match left {
             Type::BooleanLiteral(_) | Type::IntLiteral(_) => {}
-            Type::Instance(instance)
+            Type::NominalInstance(instance)
                 if matches!(
                     instance.class().known(self.db()),
                     Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool)
@@ -2517,7 +2517,7 @@ impl<'db> TypeInferenceBuilder<'db> {
             }
 
             // Super instances do not allow attribute assignment
-            Type::Instance(instance) if instance.class().is_known(db, KnownClass::Super) => {
+            Type::NominalInstance(instance) if instance.class().is_known(db, KnownClass::Super) => {
                 if emit_diagnostics {
                     if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) {
                         builder.into_diagnostic(format_args!(
@@ -2542,7 +2542,8 @@ impl<'db> TypeInferenceBuilder<'db> {
 
             Type::Dynamic(..) | Type::Never => true,
 
-            Type::Instance(..)
+            Type::NominalInstance(..)
+            | Type::ProtocolInstance(_)
             | Type::BooleanLiteral(..)
             | Type::IntLiteral(..)
             | Type::StringLiteral(..)
@@ -3033,7 +3034,7 @@ impl<'db> TypeInferenceBuilder<'db> {
         }
 
         // Handle various singletons.
-        if let Type::Instance(instance) = declared_ty.inner_type() {
+        if let Type::NominalInstance(instance) = declared_ty.inner_type() {
             if instance
                 .class()
                 .is_known(self.db(), KnownClass::SpecialForm)
@@ -5125,7 +5126,8 @@ impl<'db> TypeInferenceBuilder<'db> {
                 | Type::ClassLiteral(_)
                 | Type::GenericAlias(_)
                 | Type::SubclassOf(_)
-                | Type::Instance(_)
+                | Type::NominalInstance(_)
+                | Type::ProtocolInstance(_)
                 | Type::KnownInstance(_)
                 | Type::PropertyInstance(_)
                 | Type::Union(_)
@@ -5405,7 +5407,8 @@ impl<'db> TypeInferenceBuilder<'db> {
                 | Type::ClassLiteral(_)
                 | Type::GenericAlias(_)
                 | Type::SubclassOf(_)
-                | Type::Instance(_)
+                | Type::NominalInstance(_)
+                | Type::ProtocolInstance(_)
                 | Type::KnownInstance(_)
                 | Type::PropertyInstance(_)
                 | Type::Intersection(_)
@@ -5430,7 +5433,8 @@ impl<'db> TypeInferenceBuilder<'db> {
                 | Type::ClassLiteral(_)
                 | Type::GenericAlias(_)
                 | Type::SubclassOf(_)
-                | Type::Instance(_)
+                | Type::NominalInstance(_)
+                | Type::ProtocolInstance(_)
                 | Type::KnownInstance(_)
                 | Type::PropertyInstance(_)
                 | Type::Intersection(_)
@@ -5863,13 +5867,13 @@ impl<'db> TypeInferenceBuilder<'db> {
                     right_ty: right,
                 }),
             },
-            (Type::IntLiteral(_), Type::Instance(_)) => self.infer_binary_type_comparison(
+            (Type::IntLiteral(_), Type::NominalInstance(_)) => self.infer_binary_type_comparison(
                 KnownClass::Int.to_instance(self.db()),
                 op,
                 right,
                 range,
             ),
-            (Type::Instance(_), Type::IntLiteral(_)) => self.infer_binary_type_comparison(
+            (Type::NominalInstance(_), Type::IntLiteral(_)) => self.infer_binary_type_comparison(
                 left,
                 op,
                 KnownClass::Int.to_instance(self.db()),
@@ -5995,7 +5999,7 @@ impl<'db> TypeInferenceBuilder<'db> {
                 KnownClass::Bytes.to_instance(self.db()),
                 range,
             ),
-            (Type::Tuple(_), Type::Instance(instance))
+            (Type::Tuple(_), Type::NominalInstance(instance))
                 if instance
                     .class()
                     .is_known(self.db(), KnownClass::VersionInfo) =>
@@ -6007,7 +6011,7 @@ impl<'db> TypeInferenceBuilder<'db> {
                     range,
                 )
             }
-            (Type::Instance(instance), Type::Tuple(_))
+            (Type::NominalInstance(instance), Type::Tuple(_))
                 if instance
                     .class()
                     .is_known(self.db(), KnownClass::VersionInfo) =>
@@ -6393,7 +6397,7 @@ impl<'db> TypeInferenceBuilder<'db> {
     ) -> Type<'db> {
         match (value_ty, slice_ty) {
             (
-                Type::Instance(instance),
+                Type::NominalInstance(instance),
                 Type::IntLiteral(_) | Type::BooleanLiteral(_) | Type::SliceLiteral(_),
             ) if instance
                 .class()
@@ -6699,7 +6703,7 @@ impl<'db> TypeInferenceBuilder<'db> {
                 Err(_) => SliceArg::Unsupported,
             },
             Some(Type::BooleanLiteral(b)) => SliceArg::Arg(Some(i32::from(b))),
-            Some(Type::Instance(instance))
+            Some(Type::NominalInstance(instance))
                 if instance.class().is_known(self.db(), KnownClass::NoneType) =>
             {
                 SliceArg::Arg(None)
diff --git a/crates/red_knot_python_semantic/src/types/instance.rs b/crates/red_knot_python_semantic/src/types/instance.rs
index fb1b1deb764a4f..cfd6912597abc3 100644
--- a/crates/red_knot_python_semantic/src/types/instance.rs
+++ b/crates/red_knot_python_semantic/src/types/instance.rs
@@ -1,30 +1,54 @@
 //! Instance types: both nominal and structural.
 
+use ruff_python_ast::name::Name;
+
 use super::{ClassType, KnownClass, SubclassOfType, Type};
-use crate::Db;
+use crate::{Db, FxOrderSet};
 
 impl<'db> Type<'db> {
-    pub(crate) const fn instance(class: ClassType<'db>) -> Self {
-        Self::Instance(InstanceType { class })
+    pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self {
+        if class.class_literal(db).0.is_protocol(db) {
+            Self::ProtocolInstance(ProtocolInstanceType(Protocol::FromClass(class)))
+        } else {
+            Self::NominalInstance(NominalInstanceType { class })
+        }
     }
 
-    pub(crate) const fn into_instance(self) -> Option<InstanceType<'db>> {
+    pub(crate) const fn into_nominal_instance(self) -> Option<NominalInstanceType<'db>> {
         match self {
-            Type::Instance(instance_type) => Some(instance_type),
+            Type::NominalInstance(instance_type) => Some(instance_type),
             _ => None,
         }
     }
+
+    /// Return `true` if `self` conforms to the interface described by `protocol`.
+    ///
+    /// TODO: we may need to split this into two methods in the future, once we start
+    /// differentiating between fully-static and non-fully-static protocols.
+    pub(super) fn satisfies_protocol(
+        self,
+        db: &'db dyn Db,
+        protocol: ProtocolInstanceType<'db>,
+    ) -> bool {
+        // TODO: this should consider the types of the protocol members
+        // as well as whether each member *exists* on `self`.
+        protocol
+            .0
+            .protocol_members(db)
+            .iter()
+            .all(|member| !self.member(db, member).symbol.is_unbound())
+    }
 }
 
 /// A type representing the set of runtime objects which are instances of a certain nominal class.
 #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update)]
-pub struct InstanceType<'db> {
-    // Keep this field private, so that the only way of constructing `InstanceType` instances
+pub struct NominalInstanceType<'db> {
+    // Keep this field private, so that the only way of constructing `NominalInstanceType` instances
     // is through the `Type::instance` constructor function.
     class: ClassType<'db>,
 }
 
-impl<'db> InstanceType<'db> {
+impl<'db> NominalInstanceType<'db> {
     pub(super) fn class(self) -> ClassType<'db> {
         self.class
     }
@@ -87,8 +111,152 @@ impl<'db> InstanceType<'db> {
     }
 }
 
-impl<'db> From<InstanceType<'db>> for Type<'db> {
-    fn from(value: InstanceType<'db>) -> Self {
-        Self::Instance(value)
+impl<'db> From<NominalInstanceType<'db>> for Type<'db> {
+    fn from(value: NominalInstanceType<'db>) -> Self {
+        Self::NominalInstance(value)
+    }
+}
+
+/// A `ProtocolInstanceType` represents the set of all possible runtime objects
+/// that conform to the interface described by a certain protocol.
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, salsa::Update)]
+pub struct ProtocolInstanceType<'db>(
+    // Keep the inner field here private,
+    // so that the only way of constructing `ProtocolInstanceType` instances
+    // is through the `Type::instance` constructor function.
+    Protocol<'db>,
+);
+
+impl<'db> ProtocolInstanceType<'db> {
+    pub(super) fn inner(self) -> Protocol<'db> {
+        self.0
     }
+
+    /// Return the meta-type of this protocol-instance type.
+    pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> {
+        match self.0 {
+            Protocol::FromClass(class) => SubclassOfType::from(db, class),
+
+            // TODO: we can and should do better here.
+            //
+            // This is supported by mypy, and should be supported by us as well.
+            // We'll need to come up with a better solution for the meta-type of
+            // synthesized protocols to solve this:
+            //
+            // ```py
+            // from typing import Callable
+            //
+            // def foo(x: Callable[[], int]) -> None:
+            //     reveal_type(type(x))                 # mypy: "type[def (builtins.int) -> builtins.str]"
+            //     reveal_type(type(x).__call__)        # mypy: "def (*args: Any, **kwds: Any) -> Any"
+            // ```
+            Protocol::Synthesized(_) => KnownClass::Type.to_instance(db),
+        }
+    }
+
+    /// Return a "normalized" version of this `Protocol` type.
+    ///
+    /// See [`Type::normalized`] for more details.
+    pub(super) fn normalized(self, db: &'db dyn Db) -> Type<'db> {
+        let object = KnownClass::Object.to_instance(db);
+        if object.satisfies_protocol(db, self) {
+            return object;
+        }
+        match self.0 {
+            Protocol::FromClass(_) => Type::ProtocolInstance(Self(Protocol::Synthesized(
+                SynthesizedProtocolType::new(db, self.0.protocol_members(db)),
+            ))),
+            Protocol::Synthesized(_) => Type::ProtocolInstance(self),
+        }
+    }
+
+    /// TODO: this should return `true` if any of the members of this protocol type contain any `Todo` types.
+    #[expect(clippy::unused_self)]
+    pub(super) fn contains_todo(self) -> bool {
+        false
+    }
+
+    /// Return `true` if this protocol type is fully static.
+    ///
+    /// TODO: should not be considered fully static if any members do not have fully static types
+    #[expect(clippy::unused_self)]
+    pub(super) fn is_fully_static(self) -> bool {
+        true
+    }
+
+    /// Return `true` if this protocol type is a subtype of the protocol `other`.
+    ///
+    /// TODO: consider the types of the members as well as their existence
+    pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
+        self.0
+            .protocol_members(db)
+            .is_superset(other.0.protocol_members(db))
+    }
+
+    /// Return `true` if this protocol type is assignable to the protocol `other`.
+    ///
+    /// TODO: consider the types of the members as well as their existence
+    pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
+        self.is_subtype_of(db, other)
+    }
+
+    /// Return `true` if this protocol type is equivalent to the protocol `other`.
+    ///
+    /// TODO: consider the types of the members as well as their existence
+    pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
+        self.normalized(db) == other.normalized(db)
+    }
+
+    /// Return `true` if this protocol type is gradually equivalent to the protocol `other`.
+    ///
+    /// TODO: consider the types of the members as well as their existence
+    pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
+        self.is_equivalent_to(db, other)
+    }
+
+    /// Return `true` if this protocol type is disjoint from the protocol `other`.
+    ///
+    /// TODO: a protocol `X` is disjoint from a protocol `Y` if `X` and `Y`
+    /// have a member with the same name but disjoint types
+    #[expect(clippy::unused_self)]
+    pub(super) fn is_disjoint_from(self, _db: &'db dyn Db, _other: Self) -> bool {
+        false
+    }
+}
+
+/// An enumeration of the two kinds of protocol types: those that originate from a class
+/// definition in source code, and those that are synthesized from a set of members.
+#[derive(
+    Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, salsa::Supertype, PartialOrd, Ord,
+)]
+pub(super) enum Protocol<'db> {
+    FromClass(ClassType<'db>),
+    Synthesized(SynthesizedProtocolType<'db>),
+}
+
+impl<'db> Protocol<'db> {
+    /// Return the members of this protocol type
+    fn protocol_members(self, db: &'db dyn Db) -> &'db FxOrderSet<Name> {
+        match self {
+            Self::FromClass(class) => class
+                .class_literal(db)
+                .0
+                .into_protocol_class(db)
+                .expect("Protocol class literal should be a protocol class")
+                .protocol_members(db),
+            Self::Synthesized(synthesized) => synthesized.members(db),
+        }
+    }
+}
+
+/// A "synthesized" protocol type that is dissociated from a class definition in source code.
+///
+/// Two synthesized protocol types with the same members will share the same Salsa ID,
+/// making them easy to compare for equivalence. A synthesized protocol type is therefore
+/// returned by [`ProtocolInstanceType::normalized`] so that two protocols with the same members
+/// will be understood as equivalent even in the context of differently ordered unions or intersections.
+#[salsa::interned(debug)]
+pub(super) struct SynthesizedProtocolType<'db> {
+    #[return_ref]
+    pub(super) members: FxOrderSet<Name>,
 }
diff --git a/crates/red_knot_python_semantic/src/types/known_instance.rs b/crates/red_knot_python_semantic/src/types/known_instance.rs
index d6da1d76d42998..7a8cb2e4642ed9 100644
--- a/crates/red_knot_python_semantic/src/types/known_instance.rs
+++ b/crates/red_knot_python_semantic/src/types/known_instance.rs
@@ -1,6 +1,6 @@
 //! The `KnownInstance` type.
 //!
-//! Despite its name, this is quite a different type from [`super::InstanceType`].
+//! Despite its name, this is quite a different type from [`super::NominalInstanceType`].
 //! For the vast majority of instance-types in Python, we cannot say how many possible
 //! inhabitants there are or could be of that type at runtime. Each variant of the
 //! [`KnownInstanceType`] enum, however, represents a specific runtime symbol
@@ -260,7 +260,7 @@ impl<'db> KnownInstanceType<'db> {
     ///
     /// For example, the symbol `typing.Literal` is an instance of `typing._SpecialForm`,
     /// so `KnownInstanceType::Literal.instance_fallback(db)`
-    /// returns `Type::Instance(InstanceType { class: <typing._SpecialForm> })`.
+    /// returns `Type::NominalInstance(NominalInstanceType { class: <typing._SpecialForm> })`.
     pub(super) fn instance_fallback(self, db: &dyn Db) -> Type {
         self.class().to_instance(db)
     }
diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/red_knot_python_semantic/src/types/narrow.rs
index 19cb5c2e5d7265..a63d9d55620488 100644
--- a/crates/red_knot_python_semantic/src/types/narrow.rs
+++ b/crates/red_knot_python_semantic/src/types/narrow.rs
@@ -159,7 +159,7 @@ impl KnownConstraintFunction {
     /// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type.
     fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option<Type<'db>> {
         let constraint_fn = |class| match self {
-            KnownConstraintFunction::IsInstance => Type::instance(class),
+            KnownConstraintFunction::IsInstance => Type::instance(db, class),
             KnownConstraintFunction::IsSubclass => SubclassOfType::from(db, class),
         };
 
@@ -472,7 +472,9 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
                         union.map(db, |ty| filter_to_cannot_be_equal(db, *ty, rhs_ty))
                     }
                     // Treat `bool` as `Literal[True, False]`.
-                    Type::Instance(instance) if instance.class().is_known(db, KnownClass::Bool) => {
+                    Type::NominalInstance(instance)
+                        if instance.class().is_known(db, KnownClass::Bool) =>
+                    {
                         UnionType::from_elements(
                             db,
                             [Type::BooleanLiteral(true), Type::BooleanLiteral(false)]
@@ -501,7 +503,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
 
     fn evaluate_expr_ne(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option<Type<'db>> {
         match (lhs_ty, rhs_ty) {
-            (Type::Instance(instance), Type::IntLiteral(i))
+            (Type::NominalInstance(instance), Type::IntLiteral(i))
                 if instance.class().is_known(self.db, KnownClass::Bool) =>
             {
                 if i == 0 {
@@ -682,7 +684,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
                         let symbol = self.expect_expr_name_symbol(id);
                         constraints.insert(
                             symbol,
-                            Type::instance(rhs_class.unknown_specialization(self.db)),
+                            Type::instance(self.db, rhs_class.unknown_specialization(self.db)),
                         );
                     }
                 }
diff --git a/crates/red_knot_python_semantic/src/types/subclass_of.rs b/crates/red_knot_python_semantic/src/types/subclass_of.rs
index 52dd82aaa665ff..56bccc8b68806f 100644
--- a/crates/red_knot_python_semantic/src/types/subclass_of.rs
+++ b/crates/red_knot_python_semantic/src/types/subclass_of.rs
@@ -16,8 +16,8 @@ impl<'db> SubclassOfType<'db> {
     /// This method does not always return a [`Type::SubclassOf`] variant.
     /// If the class object is known to be a final class,
     /// this method will return a [`Type::ClassLiteral`] variant; this is a more precise type.
-    /// If the class object is `builtins.object`, `Type::Instance(<builtins.type>)` will be returned;
-    /// this is no more precise, but it is exactly equivalent to `type[object]`.
+    /// If the class object is `builtins.object`, `Type::NominalInstance(<builtins.type>)`
+    /// will be returned; this is no more precise, but it is exactly equivalent to `type[object]`.
     ///
     /// The eager normalization here means that we do not need to worry elsewhere about distinguishing
     /// between `@final` classes and other classes when dealing with [`Type::SubclassOf`] variants.
@@ -94,9 +94,9 @@ impl<'db> SubclassOfType<'db> {
         }
     }
 
-    pub(crate) fn to_instance(self) -> Type<'db> {
+    pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> {
         match self.subclass_of {
-            SubclassOfInner::Class(class) => Type::instance(class),
+            SubclassOfInner::Class(class) => Type::instance(db, class),
             SubclassOfInner::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type),
         }
     }
diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs
index ba2adf6b409a7e..d62baf9b90b3ac 100644
--- a/crates/red_knot_python_semantic/src/types/type_ordering.rs
+++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs
@@ -129,10 +129,18 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
 
         (Type::SubclassOf(_), _) => Ordering::Less,
         (_, Type::SubclassOf(_)) => Ordering::Greater,
-        (Type::Instance(left), Type::Instance(right)) => left.class().cmp(&right.class()),
 
-        (Type::Instance(_), _) => Ordering::Less,
-        (_, Type::Instance(_)) => Ordering::Greater,
+        (Type::NominalInstance(left), Type::NominalInstance(right)) => {
+            left.class().cmp(&right.class())
+        }
+        (Type::NominalInstance(_), _) => Ordering::Less,
+        (_, Type::NominalInstance(_)) => Ordering::Greater,
+
+        (Type::ProtocolInstance(left_proto), Type::ProtocolInstance(right_proto)) => {
+            left_proto.cmp(right_proto)
+        }
+        (Type::ProtocolInstance(_), _) => Ordering::Less,
+        (_, Type::ProtocolInstance(_)) => Ordering::Greater,
 
         (Type::TypeVar(left), Type::TypeVar(right)) => left.cmp(right),
         (Type::TypeVar(_), _) => Ordering::Less,


Reviews:
user: login: carljm
body: Looks great!!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T00:52:53Z
user: login: AlexWaygood
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T09:53:23Z
user: login: AlexWaygood
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T09:59:13Z
user: login: AlexWaygood
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T10:05:30Z
user: login: AlexWaygood
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T10:07:08Z
user: login: AlexWaygood
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T10:08:05Z
user: login: AlexWaygood
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T10:11:24Z
user: login: AlexWaygood
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T10:16:07Z
user: login: AlexWaygood
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T10:47:57Z
user: login: AlexWaygood
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T10:55:42Z
user: login: carljm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T14:40:51Z

Review comments:
diff_hunk: @@ -230,7 +230,7 @@ And it is also an error to use `Protocol` in type expressions:
 def f(
     x: Protocol,  # error: [invalid-type-form] "`typing.Protocol` is not allowed in type expressions"
     y: type[Protocol],  # TODO: should emit `[invalid-type-form]` here too
-) -> None:
+):
path: crates/red_knot_python_semantic/resources/mdtest/protocols.md
user: login: carljm
body: Is this just cleaning up an unnecessary annotation that doesn't add anything to the test, or is it significant somehow?
created_at: 2025-04-29T23:43:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -533,14 +533,22 @@ impl<'db> ClassLiteral<'db> {
     }
 
     /// Determine if this class is a protocol.
+    ///
+    /// See the docs on [`KnownClass::is_protocol`] for why we hardcode knowledge
+    /// about certain special-cased classes, rather than treating all classes
+    /// the same way here.
path: crates/red_knot_python_semantic/src/types/class.rs
user: login: carljm
body: This comment looks maybe out of date? I don't see any hardcoding knowledge about certain special-cased classes here.
created_at: 2025-04-29T23:48:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -533,14 +533,22 @@ impl<'db> ClassLiteral<'db> {
     }
 
     /// Determine if this class is a protocol.
+    ///
+    /// See the docs on [`KnownClass::is_protocol`] for why we hardcode knowledge
+    /// about certain special-cased classes, rather than treating all classes
+    /// the same way here.
     pub(super) fn is_protocol(self, db: &'db dyn Db) -> bool {
-        self.explicit_bases(db).iter().rev().take(3).any(|base| {
-            matches!(
-                base,
-                Type::KnownInstance(KnownInstanceType::Protocol)
-                    | Type::Dynamic(DynamicType::SubscriptedProtocol)
-            )
-        })
+        self.known(db)
+            .map(KnownClass::is_protocol)
+            .unwrap_or_else(|| {
+                self.explicit_bases(db).iter().rev().take(3).any(|base| {
path: crates/red_knot_python_semantic/src/types/class.rs
user: login: carljm
body: I realize it's not new in this PR, but I think the `.rev().take(3)` deserves a comment.
created_at: 2025-04-29T23:49:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -126,10 +126,20 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
 
         (Type::SubclassOf(_), _) => Ordering::Less,
         (_, Type::SubclassOf(_)) => Ordering::Greater,
-        (Type::Instance(left), Type::Instance(right)) => left.class().cmp(&right.class()),
 
-        (Type::Instance(_), _) => Ordering::Less,
-        (_, Type::Instance(_)) => Ordering::Greater,
+        (Type::NominalInstance(left), Type::NominalInstance(right)) => {
+            left.class().cmp(&right.class())
+        }
+        (Type::NominalInstance(_), _) => Ordering::Less,
+        (_, Type::NominalInstance(_)) => Ordering::Greater,
+
+        (Type::ProtocolInstance(left_proto), Type::ProtocolInstance(right_proto)) => {
+            debug_assert_eq!(*left, left_proto.normalized(db));
+            debug_assert_eq!(*right, right_proto.normalized(db));
path: crates/red_knot_python_semantic/src/types/type_ordering.rs
user: login: carljm
body: Is it generally invariant that we only order normalized types? Is there a more general way we should enforce/check that invariant?
created_at: 2025-04-30T00:33:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -489,7 +490,9 @@ pub enum Type<'db> {
     SubclassOf(SubclassOfType<'db>),
     /// The set of Python objects with the given class in their __class__'s method resolution order.
     /// Construct this variant using the `Type::instance` constructor function.
path: crates/red_knot_python_semantic/src/types.rs
user: login: carljm
body: Should this comment also appear for `ProtocolInstance`?
created_at: 2025-04-30T00:34:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1160,6 +1167,23 @@ impl<'db> Type<'db> {
                 false
             }
 
+            (Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
+                let call_symbol = self.member(db, "__call__").symbol;
+                match call_symbol {
+                    Symbol::Type(Type::BoundMethod(call_function), _) => call_function
+                        .into_callable_type(db)
+                        .is_subtype_of(db, target),
+                    _ => false,
+                }
+            }
+            (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
+                left.is_subtype_of(db, right)
+            }
+            // A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`.
+            // TODO: `Callable` types are also structural types.
path: crates/red_knot_python_semantic/src/types.rs
user: login: carljm
body: It's not totally clear to me what change this TODO is suggesting, since it seems like Protocol being a subtype of Callable is already handled two arms above?
created_at: 2025-04-30T00:39:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1520,6 +1504,16 @@ impl<'db> Type<'db> {
                 .into_callable_type(db)
                 .is_assignable_to(db, target),
 
+            (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
+                left.is_assignable_to(db, right)
+            }
+            // Other than the dynamic types such as `Any`/`Unknown`/`Todo` handled above,
+            // a protocol instance can never be assignable to a nominal type,
+            // with the *sole* exception of `object`.
+            // TODO: `Callable` types are also structural types.
path: crates/red_knot_python_semantic/src/types.rs
user: login: carljm
body: Same question as above
created_at: 2025-04-30T00:40:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1520,6 +1504,16 @@ impl<'db> Type<'db> {
                 .into_callable_type(db)
                 .is_assignable_to(db, target),
 
+            (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
+                left.is_assignable_to(db, right)
+            }
+            // Other than the dynamic types such as `Any`/`Unknown`/`Todo` handled above,
+            // a protocol instance can never be assignable to a nominal type,
+            // with the *sole* exception of `object`.
+            // TODO: `Callable` types are also structural types.
+            (Type::ProtocolInstance(_), _) => false,
+            (_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol),
path: crates/red_knot_python_semantic/src/types.rs
user: login: carljm
body: There will need to be an argument or a different method to distinguish fully-static `satisfies_protocol` from gradual `satisfies_protocol`.
created_at: 2025-04-30T00:41:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1540,7 +1534,16 @@ impl<'db> Type<'db> {
             }
             (Type::Tuple(left), Type::Tuple(right)) => left.is_equivalent_to(db, right),
             (Type::Callable(left), Type::Callable(right)) => left.is_equivalent_to(db, right),
-            (Type::Instance(left), Type::Instance(right)) => left.is_equivalent_to(db, right),
+            (Type::NominalInstance(left), Type::NominalInstance(right)) => {
+                left.is_equivalent_to(db, right)
+            }
+            (Type::ProtocolInstance(first), Type::ProtocolInstance(right)) => {
path: crates/red_knot_python_semantic/src/types.rs
user: login: carljm
body: `first` and `right` is a slightly odd pairing :)
created_at: 2025-04-30T00:42:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1591,6 +1594,13 @@ impl<'db> Type<'db> {
                 first.is_gradual_equivalent_to(db, second)
             }
 
+            (Type::ProtocolInstance(first), Type::ProtocolInstance(right)) => {
path: crates/red_knot_python_semantic/src/types.rs
user: login: carljm
body: another `first` and `right`
created_at: 2025-04-30T00:42:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -489,7 +490,9 @@ pub enum Type<'db> {
     SubclassOf(SubclassOfType<'db>),
     /// The set of Python objects with the given class in their __class__'s method resolution order.
     /// Construct this variant using the `Type::instance` constructor function.
-    Instance(InstanceType<'db>),
+    NominalInstance(NominalInstanceType<'db>),
path: crates/red_knot_python_semantic/src/types.rs
user: login: carljm
body: As a favor to reviewers, next time you might consider splitting out a mass rename like this into its own PR :)
created_at: 2025-04-30T00:45:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -997,22 +989,20 @@ class XReadWriteProperty:
     @x.setter
     def x(self, val: int) -> None: ...
 
-# TODO: these should pass
-static_assert(is_subtype_of(XReadWriteProperty, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XReadWriteProperty, HasXProperty))
+static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))
 
 class XClassVar:
     x: ClassVar[int] = 42
 
-static_assert(is_subtype_of(XClassVar, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XClassVar, HasXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XClassVar, HasXProperty))
+static_assert(is_assignable_to(XClassVar, HasXProperty))
path: crates/red_knot_python_semantic/resources/mdtest/protocols.md
user: login: carljm
body: Weren't these correctly failing before, and are wrongly passing now? Should there be a TODO comment here?
created_at: 2025-04-30T00:51:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -230,7 +230,7 @@ And it is also an error to use `Protocol` in type expressions:
 def f(
     x: Protocol,  # error: [invalid-type-form] "`typing.Protocol` is not allowed in type expressions"
     y: type[Protocol],  # TODO: should emit `[invalid-type-form]` here too
-) -> None:
+):
path: crates/red_knot_python_semantic/resources/mdtest/protocols.md
user: login: AlexWaygood
body: The former -- sorry, I should have commented! It was just a minor distraction for me while updating the mdtests
created_at: 2025-04-30T09:53:23Z
author_association: MEMBER
diff_hunk: @@ -997,22 +989,20 @@ class XReadWriteProperty:
     @x.setter
     def x(self, val: int) -> None: ...
 
-# TODO: these should pass
-static_assert(is_subtype_of(XReadWriteProperty, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XReadWriteProperty, HasXProperty))
+static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))
 
 class XClassVar:
     x: ClassVar[int] = 42
 
-static_assert(is_subtype_of(XClassVar, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XClassVar, HasXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XClassVar, HasXProperty))
+static_assert(is_assignable_to(XClassVar, HasXProperty))
path: crates/red_knot_python_semantic/resources/mdtest/protocols.md
user: login: AlexWaygood
body: I think it was incorrect for these to fail before, and it is correct for them to pass now? I think these were just lacking a TODO comment (sorry for missing it when adding the tests!).

The `HasXProperty` interface demands that inhabitants of the type must have an `x` attribute on them that is at least readable, and where the `x` attribute evaluates to an object of type `int` (or some subtype thereof) when it is read. `XClassVar` is a subtype of `HasXProperty`, since all inhabitants of `XClassVar` will satisfy these demands. All inhabitants of `XClassVar` have an `x` attribute on them that is readable, and where the `x` attribute evaluates to an object of type `int` (or some subtype thereof) when it is read. `XClassVar` _exceeds_ the demands of the `HasXProperty` interface, because the `x` attribute on an instance `y` of `XClassVar` can also be read on the meta-type of `y`, and indeed can be _set_ on the meta-type of `y` as well.
created_at: 2025-04-30T09:59:13Z
author_association: MEMBER
diff_hunk: @@ -489,7 +490,9 @@ pub enum Type<'db> {
     SubclassOf(SubclassOfType<'db>),
     /// The set of Python objects with the given class in their __class__'s method resolution order.
     /// Construct this variant using the `Type::instance` constructor function.
-    Instance(InstanceType<'db>),
+    NominalInstance(NominalInstanceType<'db>),
path: crates/red_knot_python_semantic/src/types.rs
user: login: AlexWaygood
body: Sorry about that :-(

It didn't feel to me like the change made sense as a standalone PR; the renaming is only a logical change in the context of us adding a new `Instance` variant alongside `NominalInstance`. And it wasn't clear to me that adding a new variant would work out well until this branch was finally compiling and panic-free.

I originally planned to try to keep a clean commit history with this PR branch (the renaming change is its own commit on this branch) so that it could be reviewed commit-by-commit. But the commit history on this branch ended up getting pretty messy due to playing Salsa-panic-whackamole over the last few days. I probably should have spent a bit longer cleaning up the commit history before moving it out of draft mode, but I've already had to fix merge conflicts several times due to the size of the branch, so I (selfishly) just wanted to open it up for review as soon as it was panic-free.

Will try to do better next time!
created_at: 2025-04-30T10:05:30Z
author_association: MEMBER
diff_hunk: @@ -1160,6 +1167,23 @@ impl<'db> Type<'db> {
                 false
             }
 
+            (Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
+                let call_symbol = self.member(db, "__call__").symbol;
+                match call_symbol {
+                    Symbol::Type(Type::BoundMethod(call_function), _) => call_function
+                        .into_callable_type(db)
+                        .is_subtype_of(db, target),
+                    _ => false,
+                }
+            }
+            (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
+                left.is_subtype_of(db, right)
+            }
+            // A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`.
+            // TODO: `Callable` types are also structural types.
path: crates/red_knot_python_semantic/src/types.rs
user: login: AlexWaygood
body: Oops, yes -- good catch! I originally planned to defer this to a followup PR, but mypy_primer highlighted several places where the PR was _introducing_ false positives due to protocols never being considered subtypes of Callable types. So I ended up just implementing it as part of this PR, since it's not too hard to do anyway. I'll get rid of the TODO.
created_at: 2025-04-30T10:07:07Z
author_association: MEMBER
diff_hunk: @@ -1520,6 +1504,16 @@ impl<'db> Type<'db> {
                 .into_callable_type(db)
                 .is_assignable_to(db, target),
 
+            (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
+                left.is_assignable_to(db, right)
+            }
+            // Other than the dynamic types such as `Any`/`Unknown`/`Todo` handled above,
+            // a protocol instance can never be assignable to a nominal type,
+            // with the *sole* exception of `object`.
+            // TODO: `Callable` types are also structural types.
+            (Type::ProtocolInstance(_), _) => false,
+            (_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol),
path: crates/red_knot_python_semantic/src/types.rs
user: login: AlexWaygood
body: Yes! For now I'll just add a TODO to the doc-comment for `satisfies_protocol()`. There'll need to be lots of changes (which I plan to make incrementally) when we start considering the types of the protocol members.
created_at: 2025-04-30T10:08:05Z
author_association: MEMBER
diff_hunk: @@ -533,14 +533,22 @@ impl<'db> ClassLiteral<'db> {
     }
 
     /// Determine if this class is a protocol.
+    ///
+    /// See the docs on [`KnownClass::is_protocol`] for why we hardcode knowledge
+    /// about certain special-cased classes, rather than treating all classes
+    /// the same way here.
path: crates/red_knot_python_semantic/src/types/class.rs
user: login: AlexWaygood
body: It's not out of date, but it's perhaps unclear. The very existence of the `KnownClass::is_protocol` method hardcodes knowledge about certain special-cased classes (and we rely on that hardcoded knowledge in this method by calling `KnownClass::is_protocol`). In the (albeit very unlikely!) event that typeshed changes the definition of `str` tomorrow to make it a protocol class, we won't automatically understand that change, because we don't iterate over the bases at all for any of our `KnownClass`es.

I think harcoding this knowledge about certain classes is justified because:
- It's honestly very unlikely that any of these `KnownClass`es will change from being a protocol class to being a non-protocol class (or vice versa)
- It avoids lots of Salsa cycles that were causing panics on early versions of this branch!
- It's probably better for performance

But it's still somewhat ugly to me that we have to have the `KnownClass::is_protocol` method at all -- I'd prefer for us to just use the generalised logic for all classes.

I'll update the comment to make it clearer.
created_at: 2025-04-30T10:11:23Z
author_association: MEMBER
diff_hunk: @@ -126,10 +126,20 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
 
         (Type::SubclassOf(_), _) => Ordering::Less,
         (_, Type::SubclassOf(_)) => Ordering::Greater,
-        (Type::Instance(left), Type::Instance(right)) => left.class().cmp(&right.class()),
 
-        (Type::Instance(_), _) => Ordering::Less,
-        (_, Type::Instance(_)) => Ordering::Greater,
+        (Type::NominalInstance(left), Type::NominalInstance(right)) => {
+            left.class().cmp(&right.class())
+        }
+        (Type::NominalInstance(_), _) => Ordering::Less,
+        (_, Type::NominalInstance(_)) => Ordering::Greater,
+
+        (Type::ProtocolInstance(left_proto), Type::ProtocolInstance(right_proto)) => {
+            debug_assert_eq!(*left, left_proto.normalized(db));
+            debug_assert_eq!(*right, right_proto.normalized(db));
path: crates/red_knot_python_semantic/src/types/type_ordering.rs
user: login: AlexWaygood
body: > Is it generally invariant that we only order normalized types?

Yes. It doesn't really make sense to order a non-normalized `UnionType`, for example, because it might contain intersections that are not ordered (or Tuples that contain unions that are not ordered, or tuples that contain unions that contain intersections that are not ordered... you get my drift :-).

> Is there a more general way we should enforce/check that invariant?

Yes, good point. I can update the various `debug_assert`s in this function.
created_at: 2025-04-30T10:16:07Z
author_association: MEMBER
diff_hunk: @@ -126,10 +126,20 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
 
         (Type::SubclassOf(_), _) => Ordering::Less,
         (_, Type::SubclassOf(_)) => Ordering::Greater,
-        (Type::Instance(left), Type::Instance(right)) => left.class().cmp(&right.class()),
 
-        (Type::Instance(_), _) => Ordering::Less,
-        (_, Type::Instance(_)) => Ordering::Greater,
+        (Type::NominalInstance(left), Type::NominalInstance(right)) => {
+            left.class().cmp(&right.class())
+        }
+        (Type::NominalInstance(_), _) => Ordering::Less,
+        (_, Type::NominalInstance(_)) => Ordering::Greater,
+
+        (Type::ProtocolInstance(left_proto), Type::ProtocolInstance(right_proto)) => {
+            debug_assert_eq!(*left, left_proto.normalized(db));
+            debug_assert_eq!(*right, right_proto.normalized(db));
path: crates/red_knot_python_semantic/src/types/type_ordering.rs
user: login: AlexWaygood
body: https://github.com/astral-sh/ruff/pull/17734 (I've rebased this PR on top of that one, and I've got rid of the new `debug_assert_eq!`s here)
created_at: 2025-04-30T10:47:57Z
author_association: MEMBER
diff_hunk: @@ -533,14 +533,22 @@ impl<'db> ClassLiteral<'db> {
     }
 
     /// Determine if this class is a protocol.
+    ///
+    /// See the docs on [`KnownClass::is_protocol`] for why we hardcode knowledge
+    /// about certain special-cased classes, rather than treating all classes
+    /// the same way here.
     pub(super) fn is_protocol(self, db: &'db dyn Db) -> bool {
-        self.explicit_bases(db).iter().rev().take(3).any(|base| {
-            matches!(
-                base,
-                Type::KnownInstance(KnownInstanceType::Protocol)
-                    | Type::Dynamic(DynamicType::SubscriptedProtocol)
-            )
-        })
+        self.known(db)
+            .map(KnownClass::is_protocol)
+            .unwrap_or_else(|| {
+                self.explicit_bases(db).iter().rev().take(3).any(|base| {
path: crates/red_knot_python_semantic/src/types/class.rs
user: login: AlexWaygood
body: added a comment explaining this
created_at: 2025-04-30T10:55:42Z
author_association: MEMBER
diff_hunk: @@ -489,7 +490,9 @@ pub enum Type<'db> {
     SubclassOf(SubclassOfType<'db>),
     /// The set of Python objects with the given class in their __class__'s method resolution order.
     /// Construct this variant using the `Type::instance` constructor function.
-    Instance(InstanceType<'db>),
+    NominalInstance(NominalInstanceType<'db>),
path: crates/red_knot_python_semantic/src/types.rs
user: login: carljm
body: No worries, just a thought! Since you raised the question, I think the multi-PR sequencing that I was imagining is that when you had the idea that maybe we should rename `Instance` to `NominalInstance` if we add `ProtocolInstance`, there's still really no pressing need to do that rename right away. Having both `Instance` and `ProtocolInstance` is maybe confusing (though I think tbh it would probably be fine even if left that way) but otherwise not a problem. So it could be left to a later follow-up.
created_at: 2025-04-30T14:37:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -997,22 +989,20 @@ class XReadWriteProperty:
     @x.setter
     def x(self, val: int) -> None: ...
 
-# TODO: these should pass
-static_assert(is_subtype_of(XReadWriteProperty, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XReadWriteProperty, HasXProperty))
+static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))
 
 class XClassVar:
     x: ClassVar[int] = 42
 
-static_assert(is_subtype_of(XClassVar, HasXProperty))  # error: [static-assert-error]
-static_assert(is_assignable_to(XClassVar, HasXProperty))  # error: [static-assert-error]
+static_assert(is_subtype_of(XClassVar, HasXProperty))
+static_assert(is_assignable_to(XClassVar, HasXProperty))
path: crates/red_knot_python_semantic/resources/mdtest/protocols.md
user: login: carljm
body: Ah of course, makes sense, I was forgetting that a classvar is of course always accessible on instances, too.
created_at: 2025-04-30T14:38:40Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-28T15:03:25Z
body: <!-- generated-comment mypy_primer -->
## `mypy_primer` results
<details>
<summary>Changes were detected when running on open source projects</summary>

```diff
packaging (https://github.com/pypa/packaging)
- error[lint:invalid-argument-type] src/packaging/metadata.py:528:43: Argument to this function is incorrect: Expected `_FormatMapMapping`, found `dict`
- error[lint:invalid-assignment] src/packaging/version.py:478:13: Object of type `Literal[0]` is not assignable to `str | bytes | SupportsInt | None`
- Found 29 diagnostics
+ Found 27 diagnostics

mypy_primer (https://github.com/hauntsaninja/mypy_primer)
- error[lint:invalid-argument-type] mypy_primer/model.py:166:40: Argument to this function is incorrect: Expected `_FormatMapMapping`, found `_FormatMap`
- error[lint:invalid-argument-type] mypy_primer/model.py:254:13: Argument to this function is incorrect: Expected `_FormatMapMapping`, found `_FormatMap`
- error[lint:invalid-argument-type] mypy_primer/model.py:303:40: Argument to this function is incorrect: Expected `_FormatMapMapping`, found `_FormatMap`
- error[lint:invalid-argument-type] mypy_primer/model.py:362:46: Argument to this function is incorrect: Expected `_FormatMapMapping`, found `_FormatMap`
- Found 20 diagnostics
+ Found 16 diagnostics

nionutils (https://github.com/nion-software/nionutils)
- error[lint:invalid-return-type] nion/utils/StructuredModel.py:84:16: Return type does not match returned value: Expected `ModelLike`, found `FieldPropertyModel`
- error[lint:invalid-return-type] nion/utils/StructuredModel.py:87:16: Return type does not match returned value: Expected `ModelLike`, found `FieldPropertyModel`
- error[lint:invalid-return-type] nion/utils/StructuredModel.py:91:16: Return type does not match returned value: Expected `ModelLike`, found `RecordModel`
- error[lint:invalid-return-type] nion/utils/StructuredModel.py:93:16: Return type does not match returned value: Expected `ModelLike`, found `ArrayModel`
- error[lint:invalid-argument-type] nion/utils/test/Binding_test.py:43:43: Argument to this function is incorrect: Expected `ObservableLike`, found `PropertyModel`
- error[lint:invalid-argument-type] nion/utils/test/ListModel_test.py:116:46: Argument to this function is incorrect: Expected `ListModelLike`, found `ListModel`
- error[lint:invalid-argument-type] nion/utils/test/ListModel_test.py:510:33: Argument to this function is incorrect: Argument type `FlattenedListModel` does not satisfy upper bound of type variable `_SupportsCloseT`
- Found 34 diagnostics
+ Found 27 diagnostics

zipp (https://github.com/jaraco/zipp)
- error[lint:invalid-argument-type] zipp/__init__.py:175:31: Argument to this function is incorrect: Expected `SizedBuffer | str`, found `Literal[b""]`
- error[lint:unsupported-operator] zipp/glob.py:4:26: Operator `*` is unsupported between objects of type `str` and `bool`
- Found 4 diagnostics
+ Found 2 diagnostics

pyp (https://github.com/hauntsaninja/pyp)
- error[lint:invalid-argument-type] pyp.py:707:9: Argument to this function is incorrect: Expected `_FormatterClass`, found `Literal[RawDescriptionHelpFormatter]`
- Found 20 diagnostics
+ Found 19 diagnostics

attrs (https://github.com/python-attrs/attrs)
- error[lint:invalid-argument-type] tests/test_funcs.py:217:13: Argument to this function is incorrect: Expected `AttrsInstance`, found `A`
- error[lint:invalid-argument-type] tests/test_funcs.py:234:48: Argument to this function is incorrect: Expected `AttrsInstance`, found `A`
- error[lint:invalid-argument-type] tests/test_funcs.py:254:13: Argument to this function is incorrect: Expected `AttrsInstance`, found `A`
- error[lint:invalid-argument-type] tests/test_funcs.py:280:25: Argument to this function is incorrect: Expected `AttrsInstance`, found `A`
- error[lint:invalid-argument-type] tests/test_funcs.py:458:13: Argument to this function is incorrect: Expected `AttrsInstance`, found `A`
- error[lint:invalid-argument-type] tests/test_funcs.py:484:26: Argument to this function is incorrect: Expected `AttrsInstance`, found `A`
- error[lint:invalid-argument-type] tests/test_hooks.py:88:42: Argument to this function is incorrect: Expected `AttrsInstance`, found `C`
- error[lint:invalid-argument-type] tests/test_hooks.py:104:50: Argument to this function is incorrect: Expected `AttrsInstance`, found `C`
- error[lint:invalid-argument-type] tests/test_hooks.py:137:48: Argument to this function is incorrect: Expected `AttrsInstance`, found `C`
- error[lint:invalid-argument-type] tests/test_hooks.py:154:48: Argument to this function is incorrect: Expected `AttrsInstance`, found `C`
- error[lint:invalid-argument-type] tests/test_hooks.py:202:40: Argument to this function is incorrect: Expected `AttrsInstance`, found `Sub`
- error[lint:invalid-argument-type] tests/test_hooks.py:268:30: Argument to this function is incorrect: Expected `AttrsInstance`, found `Parent`
- error[lint:invalid-argument-type] tests/test_hooks.py:298:21: Argument to this function is incorrect: Expected `AttrsInstance`, found `Parent`
- error[lint:invalid-argument-type] tests/test_next_gen.py:436:30: Argument to this function is incorrect: Expected `AttrsInstance`, found `C`
- error[lint:invalid-argument-type] tests/test_next_gen.py:436:53: Argument to this function is incorrect: Expected `AttrsInstance`, found `C`
- error[lint:invalid-argument-type] tests/test_next_gen.py:446:29: Argument to this function is incorrect: Expected `AttrsInstance`, found `C`
- error[lint:invalid-argument-type] tests/test_next_gen.py:447:13: Argument to this function is incorrect: Expected `AttrsInstance`, found `C`
- error[lint:invalid-argument-type] tests/test_slots.py:104:24: Argument to this function is incorrect: Expected `AttrsInstance`, found `C1Slots`
- error[lint:invalid-argument-type] tests/test_slots.py:104:54: Argument to this function is incorrect: Expected `AttrsInstance`, found `C1`
- error[lint:invalid-argument-type] tests/test_slots.py:126:44: Argument to this function is incorrect: Expected `AttrsInstance`, found `C1Slots`
- error[lint:invalid-argument-type] tests/test_slots.py:169:57: Argument to this function is incorrect: Expected `AttrsInstance`, found `C2Slots`
- error[lint:invalid-argument-type] tests/test_slots.py:268:57: Argument to this function is incorrect: Expected `AttrsInstance`, found `C2Slots`
- error[lint:invalid-argument-type] tests/test_slots.py:412:57: Argument to this function is incorrect: Expected `AttrsInstance`, found `C2Slots`
- error[lint:invalid-argument-type] tests/test_validators.py:448:21: Argument to this function is incorrect: Expected `AttrsInstance`, found `C`
- error[lint:invalid-argument-type] tests/test_validators.py:449:21: Argument to this function is incorrect: Expected `AttrsInstance`, found `C`
- error[lint:invalid-argument-type] tests/typing_example.py:425:14: Argument to this function is incorrect: Expected `AttrsInstance`, found `FactoryTest2`
- error[lint:invalid-argument-type] tests/typing_example.py:426:13: Argument to this function is incorrect: Expected `AttrsInstance`, found `FactoryTest`
- error[lint:invalid-argument-type] tests/typing_example.py:436:13: Argument to this function is incorrect: Expected `AttrsInstance`, found `FactoryTest`
- error[lint:invalid-argument-type] tests/typing_example.py:437:13: Argument to this function is incorrect: Expected `AttrsInstance`, found `FactoryTest`
- error[lint:invalid-argument-type] tests/typing_example.py:448:14: Argument to this function is incorrect: Expected `AttrsInstance`, found `MatchArgs2`
- error[lint:invalid-argument-type] tests/typing_example.py:449:15: Argument to this function is incorrect: Expected `AttrsInstance`, found `MatchArgs2`
- Found 694 diagnostics
+ Found 663 diagnostics

anyio (https://github.com/agronholm/anyio)
- error[lint:invalid-argument-type] src/anyio/_core/_tempfile.py:299:21: Argument to this function is incorrect: Argument type `BytesIO` does not satisfy upper bound of type variable `_BufferT_co`
- error[lint:invalid-argument-type] src/anyio/_core/_tempfile.py:299:21: Argument to this function is incorrect: Expected `_WrappedBuffer`, found `BytesIO`
- error[lint:invalid-argument-type] src/anyio/to_process.py:210:42: Argument to this function is incorrect: Expected `_ReadableFileobj`, found `BinaryIO | @Todo(Support for `typing.TypeAlias`)`
- error[lint:invalid-argument-type] src/anyio/to_process.py:210:42: Argument to this function is incorrect: Expected `_ReadableFileobj`, found `BinaryIO | @Todo(Support for `typing.TypeAlias`)`
- Found 140 diagnostics
+ Found 136 diagnostics

async-utils (https://github.com/mikeshardmind/async-utils)
- error[lint:invalid-return-type] src/async_utils/_paramkey.py:62:12: Return type does not match returned value: Expected `Hashable`, found `_HK`
- error[lint:invalid-argument-type] src/async_utils/bg_loop.py:113:31: Argument to this function is incorrect: Expected `_TaskFactory | None`, found `def eager_task_factory(loop: AbstractEventLoop | None, coro: @Todo(specialized non-generic class), *, name: str | None = None, context: Context | None = None) -> @Todo(specialized non-generic class)`
- error[lint:invalid-return-type] src/async_utils/corofunc_cache.py:121:12: Return type does not match returned value: Expected `CoroCacheDeco`, found `def wrapper(coro: @Todo(Generic PEP-695 type alias), /) -> @Todo(Generic PEP-695 type alias)`
- error[lint:invalid-return-type] src/async_utils/corofunc_cache.py:200:12: Return type does not match returned value: Expected `CoroCacheDeco`, found `def wrapper(coro: @Todo(Generic PEP-695 type alias), /) -> @Todo(Generic PEP-695 type alias)`
- error[lint:invalid-return-type] src/async_utils/task_cache.py:181:12: Return type does not match returned value: Expected `TaskCacheDeco`, found `def wrapper(coro: @Todo(Generic PEP-695 type alias), /) -> @Todo(Generic PEP-695 type alias)`
- error[lint:invalid-return-type] src/async_utils/task_cache.py:267:12: Return type does not match returned value: Expected `TaskCacheDeco`, found `def wrapper(coro: @Todo(Generic PEP-695 type alias), /) -> @Todo(Generic PEP-695 type alias)`
- Found 29 diagnostics
+ Found 23 diagnostics

starlette (https://github.com/encode/starlette)
- error[lint:invalid-return-type] tests/conftest.py:19:12: Return type does not match returned value: Expected `TestClientFactory`, found `partial`
- Found 193 diagnostics
+ Found 192 diagnostics

nox (https://github.com/wntrblm/nox)
- error[lint:invalid-argument-type] nox/manifest.py:271:80: Argument to this function is incorrect: Argument type `SessionRunner` does not satisfy upper bound of type variable `Node`
- Found 56 diagnostics
+ Found 55 diagnostics

pylox (https://github.com/sco1/pylox)
- error[lint:invalid-argument-type] tests/expressions/test_evaluate.py:18:31: Argument to this function is incorrect: Expected `LoxInterpreterProtocol`, found `Lox`
- error[lint:invalid-argument-type] tests/expressions/test_evaluate.py:21:29: Argument to this function is incorrect: Expected `LoxInterpreterProtocol`, found `Lox`
- error[lint:invalid-argument-type] tests/expressions/test_parse.py:19:31: Argument to this function is incorrect: Expected `LoxInterpreterProtocol`, found `Lox`
- error[lint:invalid-argument-type] tests/expressions/test_parse.py:22:29: Argument to this function is incorrect: Expected `LoxInterpreterProtocol`, found `Lox`
- error[lint:invalid-argument-type] tests/scanning/test_identifiers.py:53:33: Argument to this function is incorrect: Expected `LoxInterpreterProtocol`, found `Lox`
- error[lint:invalid-argument-type] tests/scanning/test_keywords.py:54:33: Argument to this function is incorrect: Expected `LoxInterpreterProtocol`, found `Lox`
- error[lint:invalid-argument-type] tests/scanning/test_numbers.py:31:33: Argument to this function is incorrect: Expected `LoxInterpreterProtocol`, found `Lox`
- error[lint:invalid-argument-type] tests/scanning/test_punctuators.py:53:33: Argument to this function is incorrect: Expected `LoxInterpreterProtocol`, found `Lox`
- error[lint:invalid-argument-type] tests/scanning/test_strings.py:71:33: Argument to this function is incorrect: Expected `LoxInterpreterProtocol`, found `Lox`
- error[lint:invalid-argument-type] tests/scanning/test_whitespace.py:66:33: Argument to this function is incorrect: Expected `LoxInterpreterProtocol`, found `Lox`
- Found 40 diagnostics
+ Found 30 diagnostics

ppb-vector (https://github.com/ppb/ppb-vector)
- error[lint:invalid-assignment] ppb_vector/__init__.py:291:9: Object of type `float` is not assignable to `SupportsFloat`
- error[lint:invalid-assignment] ppb_vector/__init__.py:351:9: Object of type `float` is not assignable to `SupportsFloat`
- error[lint:invalid-parameter-default] ppb_vector/__init__.py:436:17: Default value of type `float` is not assignable to annotated parameter type `SupportsFloat`
- error[lint:invalid-parameter-default] ppb_vector/__init__.py:436:49: Default value of type `float` is not assignable to annotated parameter type `SupportsFloat`
- error[lint:invalid-assignment] ppb_vector/__init__.py:459:9: Object of type `float` is not assignable to `SupportsFloat`
- error[lint:invalid-assignment] ppb_vector/__init__.py:459:18: Object of type `float` is not assignable to `SupportsFloat`
- error[lint:unsupported-operator] ppb_vector/__init__.py:460:12: Operator `<` is not supported for types `SupportsFloat` and `int`, in comparing `SupportsFloat` with `Literal[0]`
- error[lint:unsupported-operator] ppb_vector/__init__.py:460:27: Operator `<` is not supported for types `SupportsFloat` and `int`, in comparing `SupportsFloat` with `Literal[0]`
- error[lint:unsupported-operator] ppb_vector/__init__.py:472:25: Operator `*` is unsupported between objects of type `SupportsFloat` and `Unknown | int | float`
- error[lint:invalid-assignment] ppb_vector/__init__.py:548:9: Object of type `float` is not assignable to `SupportsFloat`
- error[lint:invalid-assignment] ppb_vector/__init__.py:560:9: Object of type `float` is not assignable to `SupportsFloat`
- error[lint:unsupported-operator] ppb_vector/__init__.py:561:12: Operator `<` is not supported for types `SupportsFloat` and `int`, in comparing `SupportsFloat` with `Literal[0]`
- error[lint:no-matching-overload] ppb_vector/__init__.py:622:17: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] ppb_vector/__init__.py:623:17: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] ppb_vector/__init__.py:624:15: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/benchmark.py:9:9: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/benchmark.py:10:9: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/test_decompose.py:12:8: No overload of function `__new__` matches arguments
- error[lint:invalid-argument-type] tests/test_dot.py:41:36: Argument to this function is incorrect: Expected `SupportsFloat`, found `int | float`
- error[lint:invalid-argument-type] tests/test_dot.py:41:54: Argument to this function is incorrect: Expected `SupportsFloat`, found `int | float`
- error[lint:invalid-argument-type] tests/test_isclose.py:90:22: Argument to this function is incorrect: Expected `SupportsFloat`, found `Literal[-1]`
- error[lint:invalid-argument-type] tests/test_isclose.py:93:22: Argument to this function is incorrect: Expected `SupportsFloat`, found `Literal[-1]`
- error[lint:no-matching-overload] tests/test_member_access.py:9:9: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/test_pattern_matching.py:5:11: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/test_pattern_matching.py:13:11: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/test_pattern_matching.py:21:11: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/test_pattern_matching.py:29:11: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/test_project.py:10:8: No overload of function `__new__` matches arguments
- error[lint:invalid-argument-type] tests/test_project.py:24:66: Argument to this function is incorrect: Expected `SupportsFloat`, found `Literal[90]`
- error[lint:no-matching-overload] tests/test_rotate.py:13:6: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/test_rotate.py:13:25: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/test_rotate.py:14:6: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/test_rotate.py:14:23: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/test_rotate.py:15:6: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/test_rotate.py:15:24: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/test_rotate.py:16:6: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/test_rotate.py:16:25: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/test_rotate.py:78:6: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/test_rotate.py:81:6: No overload of function `__new__` matches arguments
- error[lint:invalid-argument-type] tests/test_rotate.py:129:25: Argument to this function is incorrect: Expected `SupportsFloat`, found `int | float`
- error[lint:invalid-argument-type] tests/test_rotate.py:130:25: Argument to this function is incorrect: Expected `SupportsFloat`, found `int | float`
- error[lint:invalid-argument-type] tests/test_rotate.py:132:39: Argument to this function is incorrect: Expected `SupportsFloat`, found `int | float`
- error[lint:no-matching-overload] tests/test_rotate.py:145:15: No overload of function `__new__` matches arguments
- error[lint:invalid-argument-type] tests/test_rotate.py:155:40: Argument to this function is incorrect: Expected `SupportsFloat`, found `float`
- error[lint:no-matching-overload] tests/test_rotate.py:182:12: No overload of function `__new__` matches arguments
- error[lint:no-matching-overload] tests/test_rotate.py:182:34: No overload of function `__new__` matches arguments
- error[lint:unsupported-operator] tests/test_scalar_multiplication.py:39:13: Operator `/` is unsupported between objects of type `Vector` and `int | float`
- error[lint:invalid-argument-type] tests/test_scalar_multiplication.py:51:37: Argument to this function is incorrect: Expected `SupportsFloat`, found `int | float`
- error[lint:unsupported-operator] tests/test_scalar_multiplication.py:62:12: Operator `/` is unsupported between objects of type `Vector` and `int`
- error[lint:unsupported-operator] tests/test_scalar_multiplication.py:62:26: Operator `/` is unsupported between objects of type `Vector` and `float`
- error[lint:unsupported-operator] tests/test_scalar_multiplication.py:68:9: Operator `/` is unsupported between objects of type `Vector` and `Literal[0]`
- error[lint:unsupported-operator] tests/test_scalar_multiplication.py:71:9: Operator `/` is unsupported between objects of type `Vector` and `float`
- error[lint:invalid-argument-type] tests/test_scale.py:13:20: Argument to this function is incorrect: Expected `SupportsFloat`, found `int | float`
- error[lint:invalid-argument-type] tests/test_scale.py:20:31: Argument to this function is incorrect: Expected `SupportsFloat`, found `int | float`
- error[lint:invalid-argument-type] tests/test_scale.py:27:37: Argument to this function is incorrect: Expected `SupportsFloat`, found `int | float`
- error[lint:invalid-argument-type] tests/test_truncate.py:11:23: Argument to this function is incorrect: Expected `SupportsFloat`, found `int | float`
- error[lint:invalid-argument-type] tests/test_truncate.py:17:23: Argument to this function is incorrect: Expected `SupportsFloat`, found `int | float`
- error[lint:no-matching-overload] tests/test_truncate.py:22:7: No overload of function `__new__` matches arguments
- error[lint:invalid-argument-type] tests/test_truncate.py:35:31: Argument to this function is incorrect: Expected `SupportsFloat`, found `int | float`
- error[lint:invalid-argument-type] tests/test_truncate.py:40:28: Argument to this function is incorrect: Expected `SupportsFloat`, found `int | float`
- error[lint:invalid-argument-type] tests/test_truncate.py:48:40: Argument to this function is incorrect: Expected `SupportsFloat`, found `Literal[0]`
- error[lint:invalid-argument-type] tests/test_truncate.py:48:51: Argument to this function is incorrect: Expected `SupportsFloat`, found `float`
- error[lint:invalid-argument-type] tests/test_update.py:9:21: Argument to this function is incorrect: Expected `SupportsFloat | None`, found `int | float`
- error[lint:invalid-argument-type] tests/test_update.py:14:21: Argument to this function is incorrect: Expected `SupportsFloat | None`, found `int | float`
- error[lint:invalid-argument-type] tests/test_update.py:19:21: Argument to this function is incorrect: Expected `SupportsFloat | None`, found `int | float`
- error[lint:invalid-argument-type] tests/test_update.py:19:26: Argument to this function is incorrect: Expected `SupportsFloat | None`, found `int | float`
- Found 151 diagnostics
+ Found 85 diagnostics

flake8 (https://github.com/pycqa/flake8)
- error[lint:invalid-argument-type] src/flake8/utils.py:193:33: Argument to this function is incorrect: Argument type `BytesIO` does not satisfy upper bound of type variable `_BufferT_co`
- error[lint:invalid-argument-type] src/flake8/utils.py:193:33: Argument to this function is incorrect: Expected `_WrappedBuffer`, found `BytesIO`
- error[lint:invalid-argument-type] tests/unit/plugins/pycodestyle_test.py:25:57: Argument to this function is incorrect: Expected `LoaderProtocol | None`, found `SourceFileLoader`
- error[lint:invalid-argument-type] tests/unit/test_utils.py:188:30: Argument to this function is incorrect: Argument type `BytesIO` does not satisfy upper bound of type variable `_BufferT_co`
- error[lint:invalid-argument-type] tests/unit/test_utils.py:188:30: Argument to this function is incorrect: Expected `_WrappedBuffer`, found `BytesIO`
- error[lint:invalid-argument-type] tests/unit/test_utils.py:195:30: Argument to this function is incorrect: Argument type `BytesIO` does not satisfy upper bound of type variable `_BufferT_co`
- error[lint:invalid-argument-type] tests/unit/test_utils.py:195:30: Argument to this function is incorrect: Expected `_WrappedBuffer`, found `BytesIO`
- Found 210 diagnostics
+ Found 203 diagnostics

black (https://github.com/psf/black)
- error[lint:invalid-argument-type] src/black/__init__.py:982:17: Argument to this function is incorrect: Argument type `BinaryIO | @Todo(Support for `typing.TypeAlias`)` does not satisfy upper bound of type variable `_BufferT_co`
- error[lint:invalid-argument-type] src/black/__init__.py:982:17: Argument to this function is incorrect: Expected `_WrappedBuffer`, found `BinaryIO | @Todo(Support for `typing.TypeAlias`)`
- error[lint:invalid-argument-type] src/black/__init__.py:1027:13: Argument to this function is incorrect: Argument type `BinaryIO | @Todo(Support for `typing.TypeAlias`)` does not satisfy upper bound of type variable `_BufferT_co`
- error[lint:invalid-argument-type] src/black/__init__.py:1027:13: Argument to this function is incorrect: Expected `_WrappedBuffer`, found `BinaryIO | @Todo(Support for `typing.TypeAlias`)`
- error[lint:invalid-argument-type] src/black/__init__.py:1286:27: Argument to this function is incorrect: Argument type `BytesIO` does not satisfy upper bound of type variable `_BufferT_co`
- error[lint:invalid-argument-type] src/black/__init__.py:1286:27: Argument to this function is incorrect: Expected `_WrappedBuffer`, found `BytesIO`
- error[lint:invalid-return-type] src/blackd/middlewares.py:35:12: Return type does not match returned value: Expected `Middleware`, found `def impl(request: Request, handler: Unknown) -> @Todo(generic types.CoroutineType)`
- Found 141 diagnostics
+ Found 134 diagnostics

strawberry (https://github.com/strawberry-graphql/strawberry)
- error[lint:invalid-argument-type] strawberry/experimental/pydantic/conversion.py:106:37: Argument to this function is incorrect: Expected `DataclassInstance | type[DataclassInstance]`, found `type`
+ error[lint:invalid-argument-type] strawberry/experimental/pydantic/conversion.py:106:37: Argument to this function is incorrect: Expected `DataclassInstance`, found `type`
- error[lint:invalid-argument-type] strawberry/experimental/pydantic/utils.py:54:51: Argument to this function is incorrect: Expected `DataclassInstance | type[DataclassInstance]`, found `type`
+ error[lint:invalid-argument-type] strawberry/experimental/pydantic/utils.py:54:51: Argument to this function is incorrect: Expected `DataclassInstance`, found `type`
- error[lint:invalid-argument-type] strawberry/schema_codegen/__init__.py:199:34: Argument to this function is incorrect: Expected `HasDirectives`, found `FieldDefinitionNode | InputValueDefinitionNode`
- error[lint:invalid-argument-type] strawberry/schema_codegen/__init__.py:387:34: Argument to this function is incorrect: Expected `HasDirectives`, found `ObjectTypeDefinitionNode | ObjectTypeExtensionNode | InterfaceTypeDefinitionNode | InputObjectTypeDefinitionNode`
- Found 570 diagnostics
+ Found 568 diagnostics

mkosi (https://github.com/systemd/mkosi)
- error[lint:invalid-argument-type] mkosi/__init__.py:183:38: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:234:29: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:1105:13: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Config.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], relaxed: bool = Literal[False], tools: bool = Literal[True], scripts: Path | None = None, overlay: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()], setup: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:1111:29: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Config.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], relaxed: bool = Literal[False], tools: bool = Literal[True], scripts: Path | None = None, overlay: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()], setup: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:1154:61: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Config.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], relaxed: bool = Literal[False], tools: bool = Literal[True], scripts: Path | None = None, overlay: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()], setup: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:1253:13: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:1512:32: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:1548:9: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:1771:17: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:1831:17: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:1913:51: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:2293:59: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:2329:63: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:2377:17: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:2765:36: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Config.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], relaxed: bool = Literal[False], tools: bool = Literal[True], scripts: Path | None = None, overlay: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()], setup: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:2781:60: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Config.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], relaxed: bool = Literal[False], tools: bool = Literal[True], scripts: Path | None = None, overlay: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()], setup: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:2856:52: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Config.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], relaxed: bool = Literal[False], tools: bool = Literal[True], scripts: Path | None = None, overlay: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()], setup: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:3229:13: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:3238:17: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:3308:13: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:3397:56: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:3808:9: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:3823:13: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:3857:70: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Config.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], relaxed: bool = Literal[False], tools: bool = Literal[True], scripts: Path | None = None, overlay: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()], setup: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:3910:45: Argument to this function is incorrect: Expected `SandboxProtocol`, found `def sandbox(*, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:3928:36: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Config.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], relaxed: bool = Literal[False], tools: bool = Literal[True], scripts: Path | None = None, overlay: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()], setup: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:3952:83: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:4062:77: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:4070:85: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:4072:66: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:4079:86: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:4175:60: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Config.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], relaxed: bool = Literal[False], tools: bool = Literal[True], scripts: Path | None = None, overlay: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()], setup: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/__init__.py:4694:17: Argument to this function is incorrect: Expected `SandboxProtocol`, found `partial`
- error[lint:invalid-argument-type] mkosi/__init__.py:4741:34: Argument to this function is incorrect: Expected `SandboxProtocol`, found `partial`
- error[lint:invalid-argument-type] mkosi/__init__.py:4752:52: Argument to this function is incorrect: Expected `SandboxProtocol`, found `partial`
- error[lint:invalid-argument-type] mkosi/__init__.py:4771:17: Argument to this function is incorrect: Expected `SandboxProtocol`, found `partial`
- error[lint:invalid-parameter-default] mkosi/archive.py:25:39: Default value of type `def nosandbox(*, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)` is not assignable to annotated parameter type `SandboxProtocol`
- error[lint:invalid-parameter-default] mkosi/archive.py:71:5: Default value of type `def nosandbox(*, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)` is not assignable to annotated parameter type `SandboxProtocol`
- error[lint:invalid-parameter-default] mkosi/archive.py:114:5: Default value of type `def nosandbox(*, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)` is not assignable to annotated parameter type `SandboxProtocol`
- error[lint:invalid-argument-type] mkosi/bootloader.py:686:50: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/config.py:4295:9: Argument to this function is incorrect: Expected `_FormatterClass`, found `Literal[CustomHelpFormatter]`
- error[lint:invalid-argument-type] mkosi/config.py:5605:73: Argument to this function is incorrect: Expected `DataclassInstance | type[DataclassInstance]`, found `type[Args] | type[Config]`
+ error[lint:invalid-argument-type] mkosi/config.py:5605:73: Argument to this function is incorrect: Expected `DataclassInstance`, found `type[Args] | type[Config]`
- error[lint:invalid-parameter-default] mkosi/config.py:5834:45: Default value of type `def nosandbox(*, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)` is not assignable to annotated parameter type `SandboxProtocol`
- error[lint:invalid-argument-type] mkosi/distributions/arch.py:53:21: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/distributions/debian.py:182:21: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/installer/__init__.py:207:21: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/installer/pacman.py:226:17: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Context.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], scripts: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-parameter-default] mkosi/partition.py:36:37: Default value of type `def nosandbox(*, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)` is not assignable to annotated parameter type `SandboxProtocol`
- error[lint:invalid-argument-type] mkosi/qemu.py:172:50: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Config.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], relaxed: bool = Literal[False], tools: bool = Literal[True], scripts: Path | None = None, overlay: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()], setup: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/qemu.py:616:17: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Config.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], relaxed: bool = Literal[False], tools: bool = Literal[True], scripts: Path | None = None, overlay: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()], setup: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/qemu.py:628:25: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Config.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], relaxed: bool = Literal[False], tools: bool = Literal[True], scripts: Path | None = None, overlay: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()], setup: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/qemu.py:637:39: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Config.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], relaxed: bool = Literal[False], tools: bool = Literal[True], scripts: Path | None = None, overlay: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()], setup: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] mkosi/qemu.py:1360:61: Argument to this function is incorrect: Expected `SandboxProtocol`, found `bound method Config.sandbox(*, network: bool = Literal[False], devices: bool = Literal[False], relaxed: bool = Literal[False], tools: bool = Literal[True], scripts: Path | None = None, overlay: Path | None = None, options: @Todo(specialized non-generic class) = tuple[()], setup: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)`
- error[lint:invalid-parameter-default] mkosi/tree.py:25:19: Default value of type `def nosandbox(*, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)` is not assignable to annotated parameter type `SandboxProtocol`
- error[lint:invalid-parameter-default] mkosi/tree.py:41:5: Default value of type `def nosandbox(*, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)` is not assignable to annotated parameter type `SandboxProtocol`
- error[lint:invalid-parameter-default] mkosi/tree.py:101:5: Default value of type `def nosandbox(*, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)` is not assignable to annotated parameter type `SandboxProtocol`
- error[lint:invalid-parameter-default] mkosi/tree.py:183:26: Default value of type `def nosandbox(*, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)` is not assignable to annotated parameter type `SandboxProtocol`
- error[lint:invalid-parameter-default] mkosi/tree.py:217:5: Default value of type `def nosandbox(*, options: @Todo(specialized non-generic class) = tuple[()]) -> @Todo(specialized non-generic class)` is not assignable to annotated parameter type `SandboxProtocol`
- Found 300 diagnostics
+ Found 243 diagnostics

pip (https://github.com/pypa/pip)
- error[lint:invalid-argument-type] src/pip/_internal/index/collector.py:223:1: Argument to this function is incorrect: Expected `ParseLinks`, found `def parse_links(page: IndexContent) -> @Todo(specialized non-generic class)`
- error[lint:invalid-argument-type] src/pip/_internal/metadata/importlib/_envs.py:110:57: Argument to this function is incorrect: Expected `BasePath | None`, found `Path`
- error[lint:invalid-return-type] src/pip/_internal/operations/install/wheel.py:489:20: Return type does not match returned value: Expected `File`, found `ZipBackedFile`
- error[lint:invalid-return-type] src/pip/_internal/operations/install/wheel.py:523:20: Return type does not match returned value: Expected `File`, found `ZipBackedFile`
- error[lint:invalid-argument-type] src/pip/_internal/req/req_file.py:182:29: Argument to this function is incorrect: Expected `Iterable`, found `enumerate`
- error[lint:invalid-return-type] src/pip/_internal/resolution/resolvelib/provider.py:222:16: Return type does not match returned value: Expected `Preference`, found `tuple[bool, bool, bool, Unknown, bool, str]`
- error[lint:no-matching-overload] src/pip/_vendor/distlib/locators.py:593:27: No overload of bound method `__init__` matches arguments
- error[lint:invalid-argument-type] src/pip/_vendor/distro/distro.py:1118:29: Argument to this function is incorrect: Expected `str | _ShlexInstream | None`, found `TextIO`
- error[lint:invalid-argument-type] src/pip/_vendor/idna/codec.py:113:9: Argument to this function is incorrect: Expected `_Encoder`, found `bound method Codec.encode(data: str, errors: str = Literal["strict"]) -> tuple[bytes, int]`
- error[lint:invalid-argument-type] src/pip/_vendor/idna/codec.py:114:9: Argument to this function is incorrect: Expected `_Decoder`, found `bound method Codec.decode(data: bytes, errors: str = Literal["strict"]) -> tuple[str, int]`
- error[lint:invalid-argument-type] src/pip/_vendor/idna/codec.py:115:9: Argument to this function is incorrect: Expected `_IncrementalEncoder | None`, found `Literal[IncrementalEncoder]`
- error[lint:invalid-argument-type] src/pip/_vendor/idna/codec.py:116:9: Argument to this function is incorrect: Expected `_IncrementalDecoder | None`, found `Literal[IncrementalDecoder]`
- error[lint:invalid-argument-type] src/pip/_vendor/idna/codec.py:117:9: Argument to this function is incorrect: Expected `_StreamWriter | None`, found `Literal[StreamWriter]`
- error[lint:invalid-argument-type] src/pip/_vendor/idna/codec.py:118:9: Argument to this function is incorrect: Expected `_StreamReader | None`, found `Literal[StreamReader]`
- error[lint:invalid-argument-type] src/pip/_vendor/packaging/metadata.py:528:43: Argument to this function is incorrect: Expected `_FormatMapMapping`, found `dict`
- error[lint:invalid-assignment] src/pip/_vendor/packaging/version.py:478:13: Object of type `Literal[0]` is not assignable to `str | bytes | SupportsInt | None`
- error[lint:invalid-assignment] src/pip/_vendor/pyproject_hooks/_impl.py:146:13: Object of type `def default_subprocess_runner(cmd: @Todo(specialized non-generic class), cwd: str | None = None, extra_environ: @Todo(specialized non-generic class) | None = None) -> None` is not assignable to `SubprocessRunner | None`
- error[lint:invalid-argument-type] src/pip/_vendor/rich/control.py:198:27: Argument to this function is incorrect: Expected `_TranslateTable`, found `dict | @Todo(dict comprehension type)`
- error[lint:invalid-argument-type] src/pip/_vendor/rich/control.py:214:27: Argument to this function is incorrect: Expected `_TranslateTable`, found `dict`
- error[lint:invalid-return-type] src/pip/_vendor/rich/logging.py:205:16: Return type does not match returned value: Expected `ConsoleRenderable`, found `Text | @Todo(Type::Intersection.call())`
- error[lint:invalid-argument-type] src/pip/_vendor/rich/progress.py:1378:17: Argument to this function is incorrect: Argument type `_Reader` does not satisfy upper bound of type variable `_BufferT_co`
- error[lint:invalid-argument-type] src/pip/_vendor/rich/progress.py:1378:17: Argument to this function is incorrect: Expected `_WrappedBuffer`, found `_Reader`
- error[lint:invalid-return-type] src/pip/_vendor/rich/scope.py:62:12: Return type does not match returned value: Expected `ConsoleRenderable`, found `Panel`
- error[lint:invalid-assignment] src/pip/_vendor/rich/traceback.py:622:17: Object of type `Panel` is not assignable to `ConsoleRenderable`
- error[lint:invalid-assignment] src/pip/_vendor/rich/traceback.py:630:17: Object of type `Constrain` is not assignable to `ConsoleRenderable`
- Found 1242 diagnostics
+ Found 1217 diagnostics

isort (https://github.com/pycqa/isort)
- error[lint:invalid-argument-type] isort/io.py:47:34: Argument to this function is incorrect: Argument type `TextIOWrapper` does not satisfy upper bound of type variable `_BufferT_co`
- error[lint:invalid-argument-type] isort/io.py:47:34: Argument to this function is incorrect: Expected `_WrappedBuffer`, found `TextIOWrapper`
- Found 53 diagnostics
+ Found 51 diagnostics

pydantic (https://github.com/pydantic/pydantic)
- error[lint:invalid-return-type] pydantic/_internal/_decorators_v1.py:150:16: Return type does not match returned value: Expected `V2CoreBeforeRootValidator | V2CoreAfterRootValidator`, found `def _wrapper1(values: Unknown, _: ValidationInfo) -> Unknown`
- error[lint:invalid-return-type] pydantic/_internal/_decorators_v1.py:174:12: Return type does not match returned value: Expected `V2CoreBeforeRootValidator | V2CoreAfterRootValidator`, found `def _wrapper2(fields_tuple: Unknown, _: ValidationInfo) -> Unknown`
- error[lint:invalid-argument-type] pydantic/types.py:233:34: Argument to this function is incorrect: Expected `SupportsGt | None`, found `int | None`
- error[lint:invalid-argument-type] pydantic/types.py:233:41: Argument to this function is incorrect: Expected `SupportsGe | None`, found `int | None`
- error[lint:invalid-argument-type] pydantic/types.py:233:48: Argument to this function is incorrect: Expected `SupportsLt | None`, found `int | None`
- error[lint:invalid-argument-type] pydantic/types.py:233:55: Argument to this function is incorrect: Expected `SupportsLe | None`, found `int | None`
- error[lint:invalid-argument-type] pydantic/types.py:234:36: Argument to this function is incorrect: Expected `SupportsDiv | SupportsMod`, found `int`
- error[lint:invalid-argument-type] pydantic/types.py:238:49: Argument to this function is incorrect: Expected `SupportsGt`, found `Literal[0]`
- error[lint:invalid-argument-type] pydantic/types.py:269:49: Argument to this function is incorrect: Expected `SupportsLt`, found `Literal[0]`
- error[lint:invalid-argument-type] pydantic/types.py:300:52: Argument to this function is incorrect: Expected `SupportsLe`, found `Literal[0]`
- error[lint:invalid-argument-type] pydantic/types.py:331:52: Argument to this function is incorrect: Expected `SupportsGe`, found `Literal[0]`
- error[lint:invalid-argument-type] pydantic/types.py:494:34: Argument to this function is incorrect: Expected `SupportsGt | None`, found `int | float | None`
- error[lint:invalid-argument-type] pydantic/types.py:494:41: Argument to this function is incorrect: Expected `SupportsGe | None`, found `int | float | None`
- error[lint:invalid-argument-type] pydantic/types.py:494:48: Argument to this function is incorrect: Expected `SupportsLt | None`, found `int | float | None`
- error[lint:invalid-argument-type] pydantic/types.py:494:55: Argument to this function is incorrect: Expected `SupportsLe | None`, found `int | float | None`
- error[lint:invalid-argument-type] pydantic/types.py:495:36: Argument to this function is incorrect: Expected `SupportsDiv | SupportsMod`, found `int | float`
- error[lint:invalid-argument-type] pydantic/types.py:500:53: Argument to this function is incorrect: Expected `SupportsGt`, found `Literal[0]`
- error[lint:invalid-argument-type] pydantic/types.py:531:53: Argument to this function is incorrect: Expected `SupportsLt`, found `Literal[0]`
- error[lint:invalid-argument-type] pydantic/types.py:562:56: Argument to this function is incorrect: Expected `SupportsLe`, found `Literal[0]`
- error[lint:invalid-argument-type] pydantic/types.py:593:56: Argument to this function is incorrect: Expected `SupportsGe`, found `Literal[0]`
- error[lint:invalid-argument-type] pydantic/types.py:1128:34: Argument to this function is incorrect: Expected `SupportsGt | None`, found `int | Decimal | None`
- error[lint:invalid-argument-type] pydantic/types.py:1128:41: Argument to this function is incorrect: Expected `SupportsGe | None`, found `int | Decimal | None`
- error[lint:invalid-argument-type] pydantic/types.py:1128:48: Argument to this function is incorrect: Expected `SupportsLt | None`, found `int | Decimal | None`
- error[lint:invalid-argument-type] pydantic/types.py:1128:55: Argument to this function is incorrect: Expected `SupportsLe | None`, found `int | Decimal | None`
- error[lint:invalid-argument-type] pydantic/types.py:1129:36: Argument to this function is incorrect: Expected `SupportsDiv | SupportsMod`, found `int | Decimal`
- error[lint:invalid-argument-type] pydantic/types.py:2265:34: Argument to this function is incorrect: Expected `SupportsGt | None`, found `date | None`
- error[lint:invalid-argument-type] pydantic/types.py:2265:41: Argument to this function is incorrect: Expected `SupportsGe | None`, found `date | None`
- error[lint:invalid-argument-type] pydantic/types.py:2265:48: Argument to this function is incorrect: Expected `SupportsLt | None`, found `date | None`
- error[lint:invalid-argument-type] pydantic/types.py:2265:55: Argument to this function is incorrect: Expected `SupportsLe | None`, found `date | None`
- error[lint:invalid-argument-type] pydantic/types.py:3228:17: Argument to this function is incorrect: Expected `str | ((Any, /) -> Hashable)`, found `def _get_type_name(x: Any) -> str`
- Found 908 diagnostics
+ Found 878 diagnostics

stone (https://github.com/dropbox/stone)
- error[lint:invalid-argument-type] stone/cli.py:203:43: Argument to this function is incorrect: Argument type `BinaryIO | @Todo(Support for `typing.TypeAlias`)` does not satisfy upper bound of type variable `_BufferT_co`
- error[lint:invalid-argument-type] stone/cli.py:203:43: Argument to this function is incorrect: Expected `_WrappedBuffer`, found `BinaryIO | @Todo(Support for `typing.TypeAlias`)`
- Found 187 diagnostics
+ Found 185 diagnostics

cki-lib (https://gitlab.com/cki-project/cki-lib)
- error[lint:invalid-argument-type] cki_lib/footer.py:136:38: Argument to this function is incorrect: Expected `_FormatterClass`, found `Literal[ArgumentDefaultsHelpFormatter]`
- Found 174 diagnostics
+ Found 173 diagnostics

urllib3 (https://github.com/urllib3/urllib3)
- error[lint:invalid-argument-type] dummyserver/app.py:214:33: Argument to this function is incorrect: Argument type `GzipFile` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:no-matching-overload] dummyserver/app.py:214:33: No overload of bound method `__init__` matches arguments
- error[lint:invalid-assignment] src/urllib3/contrib/emscripten/connection.py:254:5: Object of type `EmscriptenHTTPConnection` is not assignable to `BaseHTTPConnection`
- error[lint:invalid-assignment] src/urllib3/contrib/emscripten/connection.py:255:5: Object of type `EmscriptenHTTPSConnection` is not assignable to `BaseHTTPSConnection`
- error[lint:invalid-argument-type] src/urllib3/util/ssltransport.py:164:33: Argument to this function is incorrect: Argument type `BinaryIO` does not satisfy upper bound of type variable `_BufferT_co`
- error[lint:invalid-argument-type] src/urllib3/util/ssltransport.py:164:33: Argument to this function is incorrect: Expected `_WrappedBuffer`, found `BinaryIO`
- error[lint:invalid-argument-type] test/with_dummyserver/test_connection.py:26:29: Argument to this function is incorrect: Argument type `BaseHTTPConnection` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] test/with_dummyserver/test_connection.py:34:29: Argument to this function is incorrect: Argument type `BaseHTTPConnection` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] test/with_dummyserver/test_connection.py:47:29: Argument to this function is incorrect: Argument type `BaseHTTPConnection` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] test/with_dummyserver/test_connection.py:56:29: Argument to this function is incorrect: Argument type `BaseHTTPConnection` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] test/with_dummyserver/test_connection.py:71:29: Argument to this function is incorrect: Argument type `BaseHTTPConnection` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] test/with_dummyserver/test_socketlevel.py:988:17: Argument to this function is incorrect: Argument type `SSLSocket` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] test/with_dummyserver/test_socketlevel.py:1021:17: Argument to this function is incorrect: Argument type `SSLSocket` does not satisfy upper bound of type variable `_SupportsCloseT`
- Found 483 diagnostics
+ Found 470 diagnostics

tornado (https://github.com/tornadoweb/tornado)
- error[lint:invalid-return-type] tornado/template.py:752:16: Return type does not match returned value: Expected `ContextManager`, found `Indenter`
- error[lint:invalid-return-type] tornado/template.py:765:16: Return type does not match returned value: Expected `ContextManager`, found `IncludeTemplate`
- error[lint:invalid-argument-type] tornado/test/httpclient_test.py:150:30: Argument to this function is incorrect: Expected `SizedBuffer`, found `bytes`
- error[lint:invalid-argument-type] tornado/test/httpclient_test.py:231:22: Argument to this function is incorrect: Argument type `socket` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] tornado/test/httpclient_test.py:534:22: Argument to this function is incorrect: Argument type `socket` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] tornado/test/httpserver_test.py:209:22: Argument to this function is incorrect: Argument type `IOStream` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] tornado/test/httpserver_test.py:622:51: Argument to this function is incorrect: Argument type `IOStream` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] tornado/test/httpserver_test.py:666:25: Argument to this function is incorrect: Argument type `IOStream` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] tornado/test/httpserver_test.py:835:22: Argument to this function is incorrect: Argument type `IOStream` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] tornado/test/httpserver_test.py:850:26: Argument to this function is incorrect: Argument type `IOStream` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:no-matching-overload] tornado/test/httpserver_test.py:1106:21: No overload of bound method `__init__` matches arguments
- error[lint:no-matching-overload] tornado/test/httpserver_test.py:1133:21: No overload of bound method `__init__` matches arguments
- error[lint:no-matching-overload] tornado/test/httpserver_test.py:1209:18: No overload of bound method `__init__` matches arguments
- error[lint:no-matching-overload] tornado/test/ioloop_test.py:281:21: No overload of bound method `add_handler` matches arguments
- error[lint:invalid-argument-type] tornado/test/ioloop_test.py:300:33: Argument to this function is incorrect: Argument type `socket` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] tornado/test/ioloop_test.py:305:33: Argument to this function is incorrect: Argument type `socket` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] tornado/test/simple_httpclient_test.py:213:22: Argument to this function is incorrect: Argument type `IOLoop` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] tornado/test/simple_httpclient_test.py:616:22: Argument to this function is incorrect: Argument type `AsyncHTTPClient` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] tornado/test/simple_httpclient_test.py:618:22: Argument to this function is incorrect: Argument type `AsyncHTTPClient` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] tornado/test/simple_httpclient_test.py:624:22: Argument to this function is incorrect: Argument type `AsyncHTTPClient` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] tornado/test/simple_httpclient_test.py:626:22: Argument to this function is incorrect: Argument type `AsyncHTTPClient` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:invalid-argument-type] tornado/test/simple_httpclient_test.py:628:22: Argument to this function is incorrect: Argument type `AsyncHTTPClient` does not satisfy upper bound of type variable `_SupportsCloseT`
- error[lint:no-matching-overload] tornado/test/web_test.py:2503:21: No overload of bound method `__init__` matches arguments
- error[lint:invalid-return-type] tornado/websocket.py:769:16: Return type does not match returned value: Expected `_Compressor`, found `_Compress`
- error[lint:invalid-return-type] tornado/websocket.py:806:16: Return type does not match returned value: Expected `_Decompressor`, found `_Decompress`
- Found 411 diagnostics
+ Found 386 diagnostics

jinja (https://github.com/pallets/jinja)
- error[lint:invalid-argument-type] src/jinja2/bccache.py:73:32: Argument to this function is incorrect: Expected `_ReadableFileobj`, found `BinaryIO`
- error[lint:invalid-argument-type] tests/test_bytecode_cache.py:49:36: Argument to this function is incorrect: Expected `_MemcachedClient`, found `MockMemcached`
- error[lint:invalid-argument-type] tests/test_bytecode_cache.py:64:36: Argument to this function is incorrect: Expected `_MemcachedClient`, found `MockMemcached`
- Found 448 diagnostics
+ Found 445 diagnostics

typeshed-stats (https://github.com/AlexWaygood/typeshed-stats)
- error[lint:invalid-argument-type] src/typeshed_stats/_cli.py:127:9: Argu...*[Comment body truncated]*
author_association: CONTRIBUTOR
user: login: AlexWaygood
created_at: 2025-04-28T17:51:18Z
body: Here's a minimized repro of the `pip` panic we're observing in mypy_primer:

```py
from typing import Protocol

class A(Protocol):
    def x(self) -> "B | A": ...

class B(Protocol):
    def y(self): ...

obj = record
if isinstance(obj, (B, A)):
    obj
```

EDIT: I've now fixed this panic, and added the snippet as a regression test.
author_association: MEMBER
user: login: AlexWaygood
created_at: 2025-04-29T15:00:32Z
body: > ```diff
> strawberry (https://github.com/strawberry-graphql/strawberry)
> - error[lint:invalid-argument-type] strawberry/experimental/pydantic/conversion.py:106:37: Argument to this function is incorrect: Expected `DataclassInstance | type[DataclassInstance]`, found `type`
> + error[lint:invalid-argument-type] strawberry/experimental/pydantic/conversion.py:106:37: Argument to this function is incorrect: Expected `DataclassInstance`, found `type`
> ```

This isn't a new false positive (just an error message changing slightly), but it's worth some further comment. The code is here: <https://github.com/strawberry-graphql/strawberry/blob/fc1ab18a3334784e71e178de7063d4fbb90dce55/strawberry/experimental/pydantic/conversion.py#L104-L109>. The reason we emit the error is that [`dataclasses.is_dataclass()`](https://github.com/python/typeshed/blob/12ef43c8d01032ab0991365f631f7d79a434f85a/stdlib/dataclasses.pyi#L222-L228) returns `TypeIs[DataclassInstance]` in typeshed, where `DataclassInstance` is a protocol. If we understood `TypeIs`, we'd narrow the type in that branch to one that is assignable to the function parameter's annotation, and the error would go away.
author_association: MEMBER
user: login: AlexWaygood
created_at: 2025-04-29T15:29:53Z
body: A 1-2% regression on the red-knot benchmarks: https://codspeed.io/astral-sh/ruff/branches/alex%2Fprotocol-variant-2
author_association: MEMBER
user: login: AlexWaygood
created_at: 2025-04-29T16:30:14Z
body: I spotted this line in the mypy_primer logs (it didn't make it into the comment posted by the bot):

```diff
- error[lint:invalid-argument-type] zerver/tests/test_decorators.py:1609:25: Argument to this function is incorrect: Expected `_GetResponseCallable | _AsyncGetResponseCallable`, found `(request) -> Unknown`
+ error[lint:invalid-argument-type] zerver/tests/test_decorators.py:1609:25: Argument to this function is incorrect: Expected `_GetResponseCallable`, found `(request) -> Unknown`
```

It's again not a _new_ false positive, but it deserves comment for why the existing false positive isn't going away. We don't yet understand any `Callable` types as being assignable to any `Protocol` types, and the reason for this is that we don't yet understand that all `Callable` types necessarily have a `__call__` method. (https://playknot.ruff.rs/f45263ee-cb36-4bce-8273-bd71d0543e84).

I haven't addressed this in my PR, as:
- It's not a regression in this PR, just something that isn't fixed by this PR
- The false positive would be fixed naturally if we modeled `Callable` types as synthesized protocols with a single method member (`__call__`) rather than a separate `Type` variant, and this is something I'd like to experiment with once our support for protocols is further along.

I added failing tests for this outstanding issue in https://github.com/astral-sh/ruff/pull/17682/commits/6609ecc73aaf262c307d44b5e8d8d354242bb67b.
author_association: MEMBER