Repository: pydantic/pydantic
Pull request: Support usage of `type` with `typing.Self` and type aliases
<!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->

## Change Summary

Fix errors caused by these:
```py
TypeAdapter(type[Self])

type MyInt = int
TypeAdapter(type[MyInt])
```

<!-- Please give a short summary of the changes. -->

## Related issue number

fix #10618
<!-- WARNING: please use "fix #123" style references so the issue is closed when this PR is merged. -->

## Checklist

* [x] The pull request title is a good summary of the changes - it will be used in the changelog
* [x] Unit tests for the changes exist
* [x] Tests pass on CI
* [x] Documentation reflects the changes where applicable
* [x] My PR is ready to review, **please add a comment including the phrase "please review" to assign reviewers**


Problem statement:
`type[Self]` raises TypeError in pydantic-core
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

### Traceback

```
Traceback (most recent call last):
  File "C:\Users\kchon\OneDrive\Desktop\pydantic\new.py", line 7, in <module>
    class A(BaseModel):
  File "C:\Users\kchon\OneDrive\Desktop\pydantic\pydantic\_internal\_model_construction.py", line 227, in __new__
    complete_model_class(
  File "C:\Users\kchon\OneDrive\Desktop\pydantic\pydantic\_internal\_model_construction.py", line 679, in complete_model_class
    cls.__pydantic_validator__ = create_schema_validator(
                                 ^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\kchon\OneDrive\Desktop\pydantic\pydantic\plugin\_schema_validator.py", line 50, in create_schema_validator
    return SchemaValidator(schema, config)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.SchemaError: Error building "model" validator:
  SchemaError: Error building "model-fields" validator:
  SchemaError: Field "self_field":
  SchemaError: Error building "is-subclass" validator:
  TypeError: '_SpecialForm' object cannot be converted to 'PyType'
  ```

### Example Code

```Python
from pydantic import BaseModel

class A(BaseModel):
    self_field: type[Self]
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.10.0a1                                                                                                         
        pydantic-core version: 2.24.0
          pydantic-core build: profile=release pgo=false
                 install path: C:\Users\kchon\OneDrive\Desktop\pydantic\pydantic
               python version: 3.12.1 (tags/v3.12.1:2305ca5, Dec  7 2023, 22:03:25) [MSC v.1937 64 bit (AMD64)]
                     platform: Windows-11-10.0.22631-SP0
             related packages: mypy-1.1.1 pydantic-extra-types-2.9.0 pydantic-settings-2.5.2 typing_extensions-4.12.2
                       commit: b8ef9034
```

Hints from issue discussion:

Commit message: Apply suggestions from code review

Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>

Patch to review:
diff --git a/pydantic/_internal/_generate_schema.py b/pydantic/_internal/_generate_schema.py
index 82b78722785..6abf3307f27 100644
--- a/pydantic/_internal/_generate_schema.py
+++ b/pydantic/_internal/_generate_schema.py
@@ -767,6 +767,12 @@ def _unpack_refs_defs(self, schema: CoreSchema) -> CoreSchema:
             return schema['schema']
         return schema
 
+    def _resolve_self_type(self, obj: Any) -> Any:
+        obj = self.model_type_stack.get()
+        if obj is None:
+            raise PydanticUserError('`typing.Self` is invalid in this context', code='invalid-self-type')
+        return obj
+
     def _generate_schema_from_property(self, obj: Any, source: Any) -> core_schema.CoreSchema | None:
         """Try to generate schema from either the `__get_pydantic_core_schema__` function or
         `__pydantic_core_schema__` property.
@@ -776,9 +782,7 @@ def _generate_schema_from_property(self, obj: Any, source: Any) -> core_schema.C
         """
         # avoid calling `__get_pydantic_core_schema__` if we've already visited this object
         if is_self_type(obj):
-            obj = self.model_type_stack.get()
-            if obj is None:
-                raise PydanticUserError('`typing.Self` is invalid in this context', code='invalid-self-type')
+            obj = self._resolve_self_type(obj)
         with self.defs.get_schema_or_ref(obj) as (_, maybe_schema):
             if maybe_schema is not None:
                 return maybe_schema
@@ -866,7 +870,10 @@ def _get_args_resolving_forward_refs(self, obj: Any) -> tuple[Any, ...] | None:
     def _get_args_resolving_forward_refs(self, obj: Any, required: bool = False) -> tuple[Any, ...] | None:
         args = get_args(obj)
         if args:
-            args = tuple([self._resolve_forward_ref(a) if isinstance(a, ForwardRef) else a for a in args])
+            if isinstance(obj, GenericAlias):
+                # PEP 585 generic aliases don't convert args to ForwardRefs, unlike `typing.List/Dict` etc.
+                args = (_typing_extra._make_forward_ref(a) if isinstance(a, str) else a for a in args)
+            args = (self._resolve_forward_ref(a) if isinstance(a, ForwardRef) else a for a in args)
         elif required:  # pragma: no cover
             raise TypeError(f'Expected {obj} to have generic parameters but it had none')
         return args
@@ -1659,10 +1666,15 @@ def _union_is_subclass_schema(self, union_type: Any) -> core_schema.CoreSchema:
     def _subclass_schema(self, type_: Any) -> core_schema.CoreSchema:
         """Generate schema for a Type, e.g. `Type[int]`."""
         type_param = self._get_first_arg_or_any(type_)
+        type_param = _typing_extra.eval_type(type_param, *self._types_namespace)
+
         # Assume `type[Annotated[<typ>, ...]]` is equivalent to `type[<typ>]`:
         type_param = _typing_extra.annotated_type(type_param) or type_param
+
         if type_param == Any:
             return self._type_schema()
+        elif isinstance(type_param, TypeAliasType):
+            return self.generate_schema(typing.Type[type_param.__value__])
         elif isinstance(type_param, typing.TypeVar):
             if type_param.__bound__:
                 if _typing_extra.origin_is_union(get_origin(type_param.__bound__)):
@@ -1677,6 +1689,11 @@ def _subclass_schema(self, type_: Any) -> core_schema.CoreSchema:
         elif _typing_extra.origin_is_union(get_origin(type_param)):
             return self._union_is_subclass_schema(type_param)
         else:
+            if is_self_type(type_param):
+                type_param = self._resolve_self_type(type_param)
+
+            if not inspect.isclass(type_param):
+                raise TypeError(f'Expected a class, got {type_param!r}')
             return core_schema.is_subclass_schema(type_param)
 
     def _sequence_schema(self, items_type: Any) -> core_schema.CoreSchema:
diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py
index 01d28896289..f7dce469ba1 100644
--- a/tests/test_edge_cases.py
+++ b/tests/test_edge_cases.py
@@ -25,7 +25,7 @@
 import pytest
 from dirty_equals import HasRepr, IsStr
 from pydantic_core import ErrorDetails, InitErrorDetails, PydanticSerializationError, core_schema
-from typing_extensions import Annotated, Literal, TypedDict, get_args
+from typing_extensions import Annotated, Literal, TypeAliasType, TypedDict, get_args
 
 from pydantic import (
     BaseModel,
@@ -1394,7 +1394,61 @@ class Model(BaseModel):
     assert Model.model_fields.keys() == set('abcdefg')
 
 
-def test_annotated_inside_type():
+def test_type_union():
+    class Model(BaseModel):
+        a: Type[Union[str, bytes]]
+        b: Type[Union[Any, str]]
+
+    m = Model(a=bytes, b=int)
+    assert m.model_dump() == {'a': bytes, 'b': int}
+    assert m.a == bytes
+
+
+def test_type_on_none():
+    class Model(BaseModel):
+        a: Type[None]
+
+    Model(a=type(None))
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(a=None)
+
+    assert exc_info.value.errors(include_url=False) == [
+        {
+            'type': 'is_subclass_of',
+            'loc': ('a',),
+            'msg': 'Input should be a subclass of NoneType',
+            'input': None,
+            'ctx': {'class': 'NoneType'},
+        }
+    ]
+
+
+def test_type_on_typealias():
+    Float = TypeAliasType('Float', float)
+
+    class MyFloat(float): ...
+
+    adapter = TypeAdapter(Type[Float])
+
+    adapter.validate_python(float)
+    adapter.validate_python(MyFloat)
+
+    with pytest.raises(ValidationError) as exc_info:
+        adapter.validate_python(str)
+
+    assert exc_info.value.errors(include_url=False) == [
+        {
+            'type': 'is_subclass_of',
+            'loc': (),
+            'msg': 'Input should be a subclass of float',
+            'input': str,
+            'ctx': {'class': 'float'},
+        }
+    ]
+
+
+def test_type_on_annotated():
     class Model(BaseModel):
         a: Type[Annotated[int, ...]]
 
@@ -1414,7 +1468,7 @@ class Model(BaseModel):
     ]
 
 
-def test_assign_type():
+def test_type_assign():
     class Parent:
         def echo(self):
             return 'parent'
@@ -1437,10 +1491,10 @@ class Model(BaseModel):
         Model(v=Different)
     assert exc_info.value.errors(include_url=False) == [
         {
-            'ctx': {'class': 'test_assign_type.<locals>.Parent'},
-            'input': HasRepr("<class 'tests.test_edge_cases.test_assign_type.<locals>.Different'>"),
+            'ctx': {'class': Parent.__qualname__},
+            'input': HasRepr(repr(Different)),
             'loc': ('v',),
-            'msg': 'Input should be a subclass of test_assign_type.<locals>.Parent',
+            'msg': f'Input should be a subclass of {Parent.__qualname__}',
             'type': 'is_subclass_of',
         }
     ]
@@ -2618,16 +2672,6 @@ class Model(BaseModel):
     assert m.model_dump_json() == f'{{"value":{json_value},"value_types_reversed":{json_value_reversed}}}'
 
 
-def test_type_union():
-    class Model(BaseModel):
-        a: Type[Union[str, bytes]]
-        b: Type[Union[Any, str]]
-
-    m = Model(a=bytes, b=int)
-    assert m.model_dump() == {'a': bytes, 'b': int}
-    assert m.a == bytes
-
-
 def test_model_repr_before_validation():
     log = []
 
diff --git a/tests/test_types_self.py b/tests/test_types_self.py
index 844f3e22846..b380d100a3c 100644
--- a/tests/test_types_self.py
+++ b/tests/test_types_self.py
@@ -1,13 +1,14 @@
 import dataclasses
 import re
+import sys
 import typing
-from typing import List, Optional, Union
+from typing import List, Optional, Type, Union
 
 import pytest
 import typing_extensions
 from typing_extensions import NamedTuple, TypedDict
 
-from pydantic import BaseModel, Field, PydanticUserError, TypeAdapter, ValidationError, validate_call
+from pydantic import BaseModel, Field, PydanticUserError, TypeAdapter, ValidationError, computed_field, validate_call
 
 
 @pytest.fixture(
@@ -188,3 +189,56 @@ class A(BaseModel):
             @validate_call
             def foo(self: Self):
                 pass
+
+
+def test_type_of_self(Self):
+    class A(BaseModel):
+        self_type: Type[Self]
+
+        @computed_field
+        def self_types1(self) -> List[Type[Self]]:
+            return [type(self), self.self_type]
+
+        # make sure forward refs are supported:
+        @computed_field
+        def self_types2(self) -> List[Type['Self']]:
+            return [type(self), self.self_type]
+
+        @computed_field
+        def self_types3(self) -> 'List[Type[Self]]':
+            return [type(self), self.self_type]
+
+        if sys.version_info >= (3, 9):
+            # standard container types are supported in 3.9+
+
+            @computed_field
+            def self_types4(self) -> 'list[type[Self]]':
+                return [type(self), self.self_type]
+
+            @computed_field
+            def self_types5(self) -> list['type[Self]']:
+                return [type(self), self.self_type]
+
+    class B(A): ...
+
+    A(self_type=A)
+    A(self_type=B)
+    B(self_type=B)
+
+    a = A(self_type=B)
+    for prop in (a.self_types1, a.self_types2, a.self_types3):
+        assert prop == [A, B]
+
+    for invalid_type in (type, int, A, object):
+        with pytest.raises(ValidationError) as exc_info:
+            B(self_type=invalid_type)
+
+        assert exc_info.value.errors(include_url=False) == [
+            {
+                'type': 'is_subclass_of',
+                'loc': ('self_type',),
+                'msg': f'Input should be a subclass of {B.__qualname__}',
+                'input': invalid_type,
+                'ctx': {'class': B.__qualname__},
+            }
+        ]


Code review comments:
diff_hunk: @@ -866,7 +870,10 @@ def _get_args_resolving_forward_refs(self, obj: Any) -> tuple[Any, ...] | None:
     def _get_args_resolving_forward_refs(self, obj: Any, required: bool = False) -> tuple[Any, ...] | None:
         args = get_args(obj)
         if args:
-            args = tuple([self._resolve_forward_ref(a) if isinstance(a, ForwardRef) else a for a in args])
+            if isinstance(obj, GenericAlias):
+                # PEP 585 generic aliases don't convert args to ForwardRefs, unlike `typing.List/Dict` etc.
+                args = (_typing_extra._make_forward_ref(a) if isinstance(a, str) else a for a in args)
+            args = (self._resolve_forward_ref(a) if isinstance(a, ForwardRef) else a for a in args)
path: pydantic/_internal/_generate_schema.py
text: @user1:
```suggestion
                args = tuple(_typing_extra._make_forward_ref(a) if isinstance(a, str) else a for a in args)
            args = tuple(self._resolve_forward_ref(a) if isinstance(a, ForwardRef) else a for a in args)
```
sorry