Repository: pydantic/pydantic
Pull request: Fix `mypy` issue with subclasses of `RootModel`
## Change Summary

`mypy` no longer errors when a subclass of `RootModel` is instantiated without a `root` keyword argument. See issue for details/

## Related issue number

Fix #7463

## 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
* [ ] 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**


Selected Reviewer: @lig

Problem statement:
Mypy reports "Too many positional arguments" for RootModel subclass
### Initial Checks

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

### Description


1. Subclassing the `RootModel` to use list as a root
2. Instantiating this new model without keyword `root=`
   e.g. `ListRootModel(['qwe', 'asd'])` instead of `ListRootModel(root=['qwe', 'asd'])`
3. All is working fine in runtime
4. Mypy is reporting "Too many positional arguments for ListRootModel"

I understand, that that's a minor issue, but still it would be nice to have a bit cleaner code without compromising mypy analysis

### Example Code

```Python
from pydantic import RootModel

class ListRootModel(RootModel):
    root: list[str]

ListRootModel(['qwe', 'asd'])  # mypy: Too many positional arguments for "ListRootModel"  [misc]
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.3.0
        pydantic-core version: 2.6.3
          pydantic-core build: profile=release pgo=true
                 install path: /home/nik/.cache/pypoetry/virtualenvs/zia-api-python-PZ2SEBy0-py3.11/lib/python3.11/site-packages/pydantic
               python version: 3.11.3 (main, May 15 2023, 09:03:45) [GCC 11.3.0]
                     platform: Linux-5.15.0-79-generic-x86_64-with-glibc2.35
     optional deps. installed: ['typing-extensions']
```

Hints from issue discussion:
Hi,

Thanks for your question. You should be able to use `ListRootModel.model_validate(['qwe', 'asd'])` without a `mypy` error.

See the docs on how we recommend using `RootModel` [here](https://docs.pydantic.dev/latest/usage/models/#rootmodel-and-custom-root-types).

Let us know if you have any other questions! 😄 
@sydney-runkle An additional question about this. Let's say I have a root model based on a `dict` and I want to allow calling it with no arguments so that `root` gets a value of an empty `dict` by default, how would I do this? `model_validate` doesn't allow me to call it with no arguments and it's rather long compared to just `MyModel()`. Overriding `__init__` doesn't seem to change anything: mypy still tells me:
> Missing named argument "root" for "MyModel"
Hi @berzi,

Thanks for following up - great question! The following works for me without any `mypy` issues, let me know if you're able to achieve the same.

```py
from typing import Dict

from pydantic import Field, RootModel


class MyModel(RootModel):
    root: Dict[str, str] = Field(default_factory=dict)


m = MyModel()  # no mypy problems
```
Another follow up about the original example - we can do the following with no type errors:

```py
from typing import List

from pydantic import RootModel


ListRootModel = RootModel[List[str]]
ListRootModel(['qwe', 'asd'])
```
@dmontagu, I know you've been working on some `mypy` fixes/changes, what are your thoughts on this? I'm going to reopen, as the inconsistency with the `mypy` errors (see the first example) is bugging me.

Commit message: first pass at fixing mypy argument error with subclasses of RootModel

Patch to review:
diff --git a/pydantic/mypy.py b/pydantic/mypy.py
index c2eb919472d..359e1314f19 100644
--- a/pydantic/mypy.py
+++ b/pydantic/mypy.py
@@ -440,7 +440,8 @@ def transform(self) -> bool:
                 return False
 
         is_settings = any(base.fullname == BASESETTINGS_FULLNAME for base in info.mro[:-1])
-        self.add_initializer(fields, config, is_settings)
+        is_root_model = any('pydantic.root_model.RootModel' in base.fullname for base in info.mro[:-1])
+        self.add_initializer(fields, config, is_settings, is_root_model)
         self.add_model_construct_method(fields, config, is_settings)
         self.set_frozen(fields, frozen=config.frozen is True)
 
@@ -780,7 +781,9 @@ def _infer_dataclass_attr_init_type(self, sym: SymbolTableNode, name: str, conte
 
         return default
 
-    def add_initializer(self, fields: list[PydanticModelField], config: ModelConfigData, is_settings: bool) -> None:
+    def add_initializer(
+        self, fields: list[PydanticModelField], config: ModelConfigData, is_settings: bool, is_root_model: bool
+    ) -> None:
         """Adds a fields-aware `__init__` method to the class.
 
         The added `__init__` will be annotated with types vs. all `Any` depending on the plugin settings.
@@ -799,6 +802,14 @@ def add_initializer(self, fields: list[PydanticModelField], config: ModelConfigD
                 use_alias=use_alias,
                 is_settings=is_settings,
             )
+            if is_root_model:
+                if len(fields) > 1:
+                    pass
+                    # error_extra_fields_on_root_model(self._api)
+
+                # convert root argument to positional argument
+                args[0].kind = ARG_POS
+
             if is_settings:
                 base_settings_node = self._api.lookup_fully_qualified(BASESETTINGS_FULLNAME).node
                 if '__init__' in base_settings_node.names:
@@ -1048,6 +1059,7 @@ def setdefault(self, key: str, value: Any) -> None:
 ERROR_UNEXPECTED = ErrorCode('pydantic-unexpected', 'Unexpected behavior', 'Pydantic')
 ERROR_UNTYPED = ErrorCode('pydantic-field', 'Untyped field disallowed', 'Pydantic')
 ERROR_FIELD_DEFAULTS = ErrorCode('pydantic-field', 'Invalid Field defaults', 'Pydantic')
+ERROR_EXTRA_FIELD_ROOT_MODEL = ErrorCode('pydantic-field', 'Extra field on RootModel subclass', 'Pydantic')
 
 
 def error_from_attributes(model_name: str, api: CheckerPluginInterface, context: Context) -> None:
@@ -1084,6 +1096,11 @@ def error_untyped_fields(api: SemanticAnalyzerPluginInterface, context: Context)
     api.fail('Untyped fields disallowed', context, code=ERROR_UNTYPED)
 
 
+def error_extra_fields_on_root_model(api: CheckerPluginInterface, context: Context) -> None:
+    """Emits an error when there is more than just a root field defined for a subclass of RootModel."""
+    api.fail('Only `root` is allowed as a field of a `RootModel`', context, code=ERROR_EXTRA_FIELD_ROOT_MODEL)
+
+
 def error_default_and_default_factory_specified(api: CheckerPluginInterface, context: Context) -> None:
     """Emits an error when `Field` has both `default` and `default_factory` together."""
     api.fail('Field default and default_factory cannot be specified together', context, code=ERROR_FIELD_DEFAULTS)
diff --git a/tests/mypy/modules/root_models.py b/tests/mypy/modules/root_models.py
new file mode 100644
index 00000000000..404c202bfb7
--- /dev/null
+++ b/tests/mypy/modules/root_models.py
@@ -0,0 +1,19 @@
+from typing import List
+
+from pydantic import RootModel
+
+
+class Pets1(RootModel[List[str]]):
+    pass
+
+
+Pets2 = RootModel[List[str]]
+
+
+class Pets3(RootModel):
+    root: List[str]
+
+
+pets1 = Pets1(['dog', 'cat'])
+pets2 = Pets2(['dog', 'cat'])
+pets3 = Pets3(['dog', 'cat'])
diff --git a/tests/mypy/test_mypy.py b/tests/mypy/test_mypy.py
index 782caaed1b8..435b6c846a6 100644
--- a/tests/mypy/test_mypy.py
+++ b/tests/mypy/test_mypy.py
@@ -98,6 +98,7 @@ def build(self) -> List[Union[Tuple[str, str], Any]]:
     + [
         ('mypy-plugin.ini', 'custom_constructor.py'),
         ('mypy-plugin.ini', 'generics.py'),
+        ('mypy-plugin.ini', 'root_models.py'),
         ('mypy-plugin-strict.ini', 'plugin_default_factory.py'),
         ('mypy-plugin-strict-no-any.ini', 'dataclass_no_any.py'),
         ('mypy-plugin-very-strict.ini', 'metaclass_args.py'),


Code review comments:
diff_hunk: @@ -799,6 +802,14 @@ def add_initializer(self, fields: list[PydanticModelField], config: ModelConfigD
                 use_alias=use_alias,
                 is_settings=is_settings,
             )
+            if is_root_model:
+                if len(fields) > 1:
+                    pass
+                    # error_extra_fields_on_root_model(self._api)
path: pydantic/mypy.py
text: @author:
What information should I pass as context here to this custom error func?

@user1:
Try `self._cls.info`

diff_hunk: @@ -799,6 +802,14 @@ def add_initializer(self, fields: list[PydanticModelField], config: ModelConfigD
                 use_alias=use_alias,
                 is_settings=is_settings,
             )
+            if is_root_model:
+                if len(fields) > 1:
+                    pass
+                    # error_extra_fields_on_root_model(self._api)
+
+                # convert root argument to positional argument
+                args[0].kind = ARG_POS
path: pydantic/mypy.py
text: @user1:
Might need to check if there's a default, I'm looking more closely..