Repository: pydantic/pydantic
Pull request: fix: Allow inheriting dataclasses with defaults
<!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->

## Change Summary

This patch allows for a `pydantic.Basemodel` class to inherit a "pure" stdlib dataclass, even when the attribute name is set on the class instance, as is the case with `dataclasses.field` and default assignments.

## Related issue number

fix #6984

## 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**


Selected Reviewer: @hramezani

Problem statement:
Allow inheriting `dataclasses.dataclass` with defaults
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

Currently, inheriting a pure dataclass to a subclass that is also a `BaseModel` works. This is great, and this feature has been used extensively in v1 in a code base i maintain. v2 seems to have the added feature of not requiring to re-declare the fields, which is awesome.

However, if the dataclass defines a default for a field, it crashes with a `NameError`

```console
Traceback (most recent call last):
...
  File "<input>", line 19, in <module>
  File ".../lib/python3.10/site-packages/pydantic/_internal/_model_construction.py", line 172, in __new__
    set_model_fields(cls, bases, config_wrapper, types_namespace)
  File ".../lib/python3.10/site-packages/pydantic/_internal/_model_construction.py", line 400, in set_model_fields
    fields, class_vars = collect_model_fields(cls, bases, config_wrapper, types_namespace, typevars_map=typevars_map)
  File ".../lib/python3.10/site-packages/pydantic/_internal/_fields.py", line 155, in collect_model_fields
    raise NameError(
NameError: Field name "a" shadows an attribute in parent "A"; you might want to use a different field name with "alias='a'".
```

### Example Code

```Python
import pydantic

from dataclasses import dataclass

import pydantic


@dataclass
class A:
    a: None

class B(pydantic.BaseModel, A): # Ok
    pass


@dataclass
class A:
    a: None = None
    

class B(pydantic.BaseModel, A): # Not ok
    pass
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.1.1
        pydantic-core version: 2.4.0
          pydantic-core build: profile=release pgo=true mimalloc=true
                 install path: .../lib/python3.10/site-packages/pydantic
               python version: 3.10.2 (main, Mar  3 2022, 12:03:29) [GCC 9.3.0]
                     platform: Linux-5.14.0-1054-oem-x86_64-with-glibc2.35
     optional deps. installed: ['typing-extensions']
```


Selected Assignee: @lig

Hints from issue discussion:
@tobni thank you for the bug report.

I can confirm that the issue reproduces in Pydantic v2.1.1.

Commit message: test: Add explanatory docstring and showcase supported use

Patch to review:
diff --git a/pydantic/_internal/_fields.py b/pydantic/_internal/_fields.py
index 61ba44696df..92a240f547e 100644
--- a/pydantic/_internal/_fields.py
+++ b/pydantic/_internal/_fields.py
@@ -147,10 +147,18 @@ def collect_model_fields(  # noqa: C901
         # "... shadows an attribute" errors
         generic_origin = getattr(cls, '__pydantic_generic_metadata__', {}).get('origin')
         for base in bases:
+            dataclass_fields = {
+                field.name for field in (dataclasses.fields(base) if dataclasses.is_dataclass(base) else ())
+            }
             if hasattr(base, ann_name):
                 if base is generic_origin:
                     # Don't error when "shadowing" of attributes in parametrized generics
                     continue
+
+                if ann_name in dataclass_fields:
+                    # Don't error when inheriting stdlib dataclasses whose fields are "shadowed" by defaults being set
+                    # on the class instance.
+                    continue
                 warnings.warn(
                     f'Field name "{ann_name}" shadows an attribute in parent "{base.__qualname__}"; ',
                     UserWarning,
diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py
index 63a385abffb..ffd578b3a44 100644
--- a/tests/test_dataclasses.py
+++ b/tests/test_dataclasses.py
@@ -2500,3 +2500,50 @@ class StdLibDataclass:
 
     assert is_pydantic_dataclass(PydanticDataclass) is True
     assert is_pydantic_dataclass(StdLibDataclass) is False
+
+
+def test_can_inherit_stdlib_dataclasses_with_defaults():
+    @dataclasses.dataclass
+    class Base:
+        a: None = None
+
+    class Model(BaseModel, Base):
+        pass
+
+    assert Model().a is None
+
+
+@pytest.mark.xfail(raises=ValidationError, require=True)
+def test_can_inherit_stdlib_dataclasses_default_factories_and_use_them():
+    """This test documents that default factories are not supported"""
+
+    @dataclasses.dataclass
+    class Base:
+        a: str = dataclasses.field(default_factory=lambda: 'TEST')
+
+    class Model(BaseModel, Base):
+        pass
+
+    assert Model().a == 'TEST'
+
+
+def test_can_inherit_stdlib_dataclasses_default_factories_and_provide_a_value():
+    @dataclasses.dataclass
+    class Base:
+        a: str = dataclasses.field(default_factory=lambda: 'TEST')
+
+    class Model(BaseModel, Base):
+        pass
+
+    assert Model(a='NOT_THE_SAME').a == 'NOT_THE_SAME'
+
+
+def test_can_inherit_stdlib_dataclasses_with_dataclass_fields():
+    @dataclasses.dataclass
+    class Base:
+        a: int = dataclasses.field(default=5)
+
+    class Model(BaseModel, Base):
+        pass
+
+    assert Model().a == 5


Code review comments:
diff_hunk: @@ -2500,3 +2500,50 @@ class StdLibDataclass:
 
     assert is_pydantic_dataclass(PydanticDataclass) is True
     assert is_pydantic_dataclass(StdLibDataclass) is False
+
+
+def test_can_inherit_stdlib_dataclasses_with_defaults():
+    @dataclasses.dataclass
+    class Base:
+        a: None = None
+
+    class Model(BaseModel, Base):
+        pass
+
+    assert Model().a is None
+
+
+@pytest.mark.xfail(raises=ValidationError, require=True)
+def test_can_inherit_stdlib_dataclasses_default_factories_and_use_them():
+    """This test documents that default factories are not supported"""
+
+    @dataclasses.dataclass
+    class Base:
+        a: str = dataclasses.field(default_factory=lambda: 'TEST')
+
+    class Model(BaseModel, Base):
+        pass
+
+    assert Model().a == 'TEST'
path: tests/test_dataclasses.py
text: @user1:
I'd rather see `with pytest.raises(...)` since that's what we use elsewhere