Repository: pydantic/pydantic
Issue category: Feature Request

Apply `strict=True` to `__init__` in mypy plugin
### Initial Checks

- [X] I have searched Google & GitHub for similar requests and couldn't find anything
- [X] I have read and followed [the docs](https://docs.pydantic.dev) and still think this feature is missing

### Description

Currently when `mypy` plugin is enabled, we can only enable/disable type checking of `__init__` in plugin level via `init_typed = True`. I'd like the type annotations to also apply to models or fields with `strict=True` even when `init_typed` is set to false.

I have PR ready (#9998). Since this is my first time working with the `mypy` plugin, I would greatly appreciate any feedback or suggestions.

#### Example Code

```python
class Model(BaseModel):
    a: int
    b: int = Field(strict=True)
    c: int = Field(strict=False)

# expected mypy error: b
Model(a='1', b='2', c='3')


class ModelStrictMode(BaseModel):
    model_config = {'strict': True}

    a: int
    b: int = Field(strict=True)
    c: int = Field(strict=False)

# expected mypy error: a, b
ModelStrictMode(a='1', b='2', c='3')
```

### Affected Components

- [ ] [Compatibility between releases](https://docs.pydantic.dev/changelog/)
- [ ] [Data validation/parsing](https://docs.pydantic.dev/concepts/models/#basic-model-usage)
- [ ] [Data serialization](https://docs.pydantic.dev/concepts/serialization/) - `.model_dump()` and `.model_dump_json()`
- [ ] [JSON Schema](https://docs.pydantic.dev/concepts/json_schema/)
- [ ] [Dataclasses](https://docs.pydantic.dev/concepts/dataclasses/)
- [ ] [Model Config](https://docs.pydantic.dev/concepts/config/)
- [ ] [Field Types](https://docs.pydantic.dev/api/types/) - adding or changing a particular data type
- [ ] [Function validation decorator](https://docs.pydantic.dev/concepts/validation_decorator/)
- [ ] [Generic Models](https://docs.pydantic.dev/concepts/models/#generic-models)
- [ ] [Other Model behaviour](https://docs.pydantic.dev/concepts/models/) - `model_construct()`, pickling, private attributes, ORM mode
- [X] [Plugins](https://docs.pydantic.dev/) and integration with other tools - mypy, FastAPI, python-devtools, Hypothesis, VS Code, PyCharm, etc.


Functions to edit:
pydantic/mypy.py:PydanticModelField.__init__
pydantic/mypy.py:PydanticModelField.to_argument
pydantic/mypy.py:PydanticModelField.serialize
pydantic/mypy.py:PydanticModelTransformer
pydantic/mypy.py:PydanticModelTransformer.collect_field_or_class_var_from_stmt
pydantic/mypy.py:PydanticModelTransformer.add_initializer
pydantic/mypy.py:PydanticModelTransformer.add_model_construct_method
pydantic/mypy.py:PydanticModelTransformer.get_field_arguments
pydantic/mypy.py:ModelConfigData.__init__
Functions added: pydantic/mypy.py:PydanticModelTransformer.get_strict
pydantic/mypy.py:PydanticModelTransformer

Patch:
diff --git a/pydantic/mypy.py b/pydantic/mypy.py
index 4c6e80f289..b001680a99 100644
--- a/pydantic/mypy.py
+++ b/pydantic/mypy.py
@@ -324,6 +324,7 @@ def __init__(
         is_frozen: bool,
         has_dynamic_alias: bool,
         has_default: bool,
+        strict: bool | None,
         line: int,
         column: int,
         type: Type | None,
@@ -334,6 +335,7 @@ def __init__(
         self.is_frozen = is_frozen
         self.has_dynamic_alias = has_dynamic_alias
         self.has_default = has_default
+        self.strict = strict
         self.line = line
         self.column = column
         self.type = type
@@ -343,6 +345,7 @@ def to_argument(
         self,
         current_info: TypeInfo,
         typed: bool,
+        model_strict: bool,
         force_optional: bool,
         use_alias: bool,
         api: SemanticAnalyzerPluginInterface,
@@ -351,7 +354,13 @@ def to_argument(
     ) -> Argument:
         """Based on mypy.plugins.dataclasses.DataclassAttribute.to_argument."""
         variable = self.to_var(current_info, api, use_alias, force_typevars_invariant)
-        type_annotation = self.expand_type(current_info, api) if typed else AnyType(TypeOfAny.explicit)
+
+        strict = model_strict if self.strict is None else self.strict
+        if typed or strict:
+            type_annotation = self.expand_type(current_info, api)
+        else:
+            type_annotation = AnyType(TypeOfAny.explicit)
+
         return Argument(
             variable=variable,
             type_annotation=type_annotation,
@@ -414,6 +423,7 @@ def serialize(self) -> JsonDict:
             'is_frozen': self.is_frozen,
             'has_dynamic_alias': self.has_dynamic_alias,
             'has_default': self.has_default,
+            'strict': self.strict,
             'line': self.line,
             'column': self.column,
             'type': self.type.serialize(),
@@ -473,6 +483,7 @@ class PydanticModelTransformer:
         'from_attributes',
         'populate_by_name',
         'alias_generator',
+        'strict',
     }
 
     def __init__(
@@ -796,6 +807,7 @@ def collect_field_or_class_var_from_stmt(  # noqa C901
             )
 
         has_default = self.get_has_default(stmt)
+        strict = self.get_strict(stmt)
 
         if sym.type is None and node.is_final and node.is_inferred:
             # This follows the logic from the dataclasses plugin. The following comment is taken verbatim:
@@ -825,6 +837,7 @@ def collect_field_or_class_var_from_stmt(  # noqa C901
             name=lhs.name,
             has_dynamic_alias=has_dynamic_alias,
             has_default=has_default,
+            strict=strict,
             alias=alias,
             is_frozen=is_frozen,
             line=stmt.line,
@@ -882,11 +895,13 @@ def add_initializer(
             return  # Don't generate an __init__ if one already exists
 
         typed = self.plugin_config.init_typed
+        model_strict = bool(config.strict)
         use_alias = config.populate_by_name is not True
         requires_dynamic_aliases = bool(config.has_alias_generator and not config.populate_by_name)
         args = self.get_field_arguments(
             fields,
             typed=typed,
+            model_strict=model_strict,
             requires_dynamic_aliases=requires_dynamic_aliases,
             use_alias=use_alias,
             is_settings=is_settings,
@@ -937,6 +952,7 @@ def add_model_construct_method(
             args = self.get_field_arguments(
                 fields,
                 typed=True,
+                model_strict=bool(config.strict),
                 requires_dynamic_aliases=False,
                 use_alias=False,
                 is_settings=is_settings,
@@ -1045,6 +1061,22 @@ def get_has_default(stmt: AssignmentStmt) -> bool:
         # Has no default if the "default value" is Ellipsis (i.e., `field_name: Annotation = ...`)
         return not isinstance(expr, EllipsisExpr)
 
+    @staticmethod
+    def get_strict(stmt: AssignmentStmt) -> bool | None:
+        """Returns a the `strict` value of a field if defined, otherwise `None`."""
+        expr = stmt.rvalue
+        if isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr) and expr.callee.fullname == FIELD_FULLNAME:
+            for arg, name in zip(expr.args, expr.arg_names):
+                if name != 'strict':
+                    continue
+                if isinstance(arg, NameExpr):
+                    if arg.fullname == 'builtins.True':
+                        return True
+                    elif arg.fullname == 'builtins.False':
+                        return False
+                return None
+        return None
+
     @staticmethod
     def get_alias_info(stmt: AssignmentStmt) -> tuple[str | None, bool]:
         """Returns a pair (alias, has_dynamic_alias), extracted from the declaration of the field defined in `stmt`.
@@ -1102,6 +1134,7 @@ def get_field_arguments(
         self,
         fields: list[PydanticModelField],
         typed: bool,
+        model_strict: bool,
         use_alias: bool,
         requires_dynamic_aliases: bool,
         is_settings: bool,
@@ -1117,6 +1150,7 @@ def get_field_arguments(
             field.to_argument(
                 info,
                 typed=typed,
+                model_strict=model_strict,
                 force_optional=requires_dynamic_aliases or is_settings,
                 use_alias=use_alias,
                 api=self._api,
@@ -1166,12 +1200,14 @@ def __init__(
         from_attributes: bool | None = None,
         populate_by_name: bool | None = None,
         has_alias_generator: bool | None = None,
+        strict: bool | None = None,
     ):
         self.forbid_extra = forbid_extra
         self.frozen = frozen
         self.from_attributes = from_attributes
         self.populate_by_name = populate_by_name
         self.has_alias_generator = has_alias_generator
+        self.strict = strict
 
     def get_values_dict(self) -> dict[str, Any]:
         """Returns a dict of Pydantic model config names to their values.

Test patch: diff --git a/tests/mypy/modules/plugin_strict_fields.py b/tests/mypy/modules/plugin_strict_fields.py
new file mode 100644
index 0000000000..1307b35950
--- /dev/null
+++ b/tests/mypy/modules/plugin_strict_fields.py
@@ -0,0 +1,49 @@
+from pydantic import BaseModel, Field
+
+
+class Model(BaseModel):
+    a: int
+    b: int = Field(strict=True)
+    c: int = Field(strict=False)
+
+
+# expected error: b
+Model(a='1', b='2', c='3')
+
+
+class ModelStrictMode(BaseModel):
+    model_config = {'strict': True}
+
+    a: int
+    b: int = Field(strict=True)
+    c: int = Field(strict=False)
+
+
+# expected error: a, b
+ModelStrictMode(a='1', b='2', c='3')
+
+
+class ModelOverride1(Model):
+    b: int = Field(strict=False)
+    c: int = Field(strict=True)
+
+
+# expected error: c
+ModelOverride1(a='1', b='2', c='3')
+
+
+class ModelOverride2(ModelStrictMode):
+    b: int = Field(strict=False)
+    c: int = Field(strict=True)
+
+
+# expected error: a, c
+ModelOverride2(a='1', b='2', c='3')
+
+
+class ModelOverrideStrictMode(ModelStrictMode):
+    model_config = {'strict': False}
+
+
+# expected error: b
+ModelOverrideStrictMode(a='1', b='2', c='3')
diff --git a/tests/mypy/outputs/1.0.1/mypy-plugin_ini/plugin_strict_fields.py b/tests/mypy/outputs/1.0.1/mypy-plugin_ini/plugin_strict_fields.py
new file mode 100644
index 0000000000..7cea5ba47b
--- /dev/null
+++ b/tests/mypy/outputs/1.0.1/mypy-plugin_ini/plugin_strict_fields.py
@@ -0,0 +1,56 @@
+from pydantic import BaseModel, Field
+
+
+class Model(BaseModel):
+    a: int
+    b: int = Field(strict=True)
+    c: int = Field(strict=False)
+
+
+# expected error: b
+Model(a='1', b='2', c='3')
+# MYPY: error: Argument "b" to "Model" has incompatible type "str"; expected "int"  [arg-type]
+
+
+class ModelStrictMode(BaseModel):
+    model_config = {'strict': True}
+
+    a: int
+    b: int = Field(strict=True)
+    c: int = Field(strict=False)
+
+
+# expected error: a, b
+ModelStrictMode(a='1', b='2', c='3')
+# MYPY: error: Argument "a" to "ModelStrictMode" has incompatible type "str"; expected "int"  [arg-type]
+# MYPY: error: Argument "b" to "ModelStrictMode" has incompatible type "str"; expected "int"  [arg-type]
+
+
+class ModelOverride1(Model):
+    b: int = Field(strict=False)
+    c: int = Field(strict=True)
+
+
+# expected error: c
+ModelOverride1(a='1', b='2', c='3')
+# MYPY: error: Argument "c" to "ModelOverride1" has incompatible type "str"; expected "int"  [arg-type]
+
+
+class ModelOverride2(ModelStrictMode):
+    b: int = Field(strict=False)
+    c: int = Field(strict=True)
+
+
+# expected error: a, c
+ModelOverride2(a='1', b='2', c='3')
+# MYPY: error: Argument "a" to "ModelOverride2" has incompatible type "str"; expected "int"  [arg-type]
+# MYPY: error: Argument "c" to "ModelOverride2" has incompatible type "str"; expected "int"  [arg-type]
+
+
+class ModelOverrideStrictMode(ModelStrictMode):
+    model_config = {'strict': False}
+
+
+# expected error: b
+ModelOverrideStrictMode(a='1', b='2', c='3')
+# MYPY: error: Argument "b" to "ModelOverrideStrictMode" has incompatible type "str"; expected "int"  [arg-type]
diff --git a/tests/mypy/outputs/1.1.1/mypy-plugin_ini/plugin_strict_fields.py b/tests/mypy/outputs/1.1.1/mypy-plugin_ini/plugin_strict_fields.py
new file mode 100644
index 0000000000..7cea5ba47b
--- /dev/null
+++ b/tests/mypy/outputs/1.1.1/mypy-plugin_ini/plugin_strict_fields.py
@@ -0,0 +1,56 @@
+from pydantic import BaseModel, Field
+
+
+class Model(BaseModel):
+    a: int
+    b: int = Field(strict=True)
+    c: int = Field(strict=False)
+
+
+# expected error: b
+Model(a='1', b='2', c='3')
+# MYPY: error: Argument "b" to "Model" has incompatible type "str"; expected "int"  [arg-type]
+
+
+class ModelStrictMode(BaseModel):
+    model_config = {'strict': True}
+
+    a: int
+    b: int = Field(strict=True)
+    c: int = Field(strict=False)
+
+
+# expected error: a, b
+ModelStrictMode(a='1', b='2', c='3')
+# MYPY: error: Argument "a" to "ModelStrictMode" has incompatible type "str"; expected "int"  [arg-type]
+# MYPY: error: Argument "b" to "ModelStrictMode" has incompatible type "str"; expected "int"  [arg-type]
+
+
+class ModelOverride1(Model):
+    b: int = Field(strict=False)
+    c: int = Field(strict=True)
+
+
+# expected error: c
+ModelOverride1(a='1', b='2', c='3')
+# MYPY: error: Argument "c" to "ModelOverride1" has incompatible type "str"; expected "int"  [arg-type]
+
+
+class ModelOverride2(ModelStrictMode):
+    b: int = Field(strict=False)
+    c: int = Field(strict=True)
+
+
+# expected error: a, c
+ModelOverride2(a='1', b='2', c='3')
+# MYPY: error: Argument "a" to "ModelOverride2" has incompatible type "str"; expected "int"  [arg-type]
+# MYPY: error: Argument "c" to "ModelOverride2" has incompatible type "str"; expected "int"  [arg-type]
+
+
+class ModelOverrideStrictMode(ModelStrictMode):
+    model_config = {'strict': False}
+
+
+# expected error: b
+ModelOverrideStrictMode(a='1', b='2', c='3')
+# MYPY: error: Argument "b" to "ModelOverrideStrictMode" has incompatible type "str"; expected "int"  [arg-type]
diff --git a/tests/mypy/test_mypy.py b/tests/mypy/test_mypy.py
index 17167991df..a5ec218dc3 100644
--- a/tests/mypy/test_mypy.py
+++ b/tests/mypy/test_mypy.py
@@ -110,6 +110,7 @@ def build(self) -> List[Union[Tuple[str, str], Any]]:
         ('mypy-plugin.ini', 'plugin_optional_inheritance.py'),
         ('mypy-plugin.ini', 'generics.py'),
         ('mypy-plugin.ini', 'root_models.py'),
+        ('mypy-plugin.ini', 'plugin_strict_fields.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'),