Repository: pydantic/pydantic
Pull request: Docs update and warning frozen defined both
## Change Summary
This PR addresses an issue where defining `frozen` both in the `@dataclass` decorator and `config=ConfigDict(frozen=True)` led to confusing and inconsistent behavior. 
The changes made in this PR aim to clarify the expected behavior and provide better guidance to users.

- docs change
```
    It's not recommended to define both `frozen` in the `dataclass` decorator and `ConfigDict`.
    If both are defined, the setting from the dataclass decorator will take priority.
```

In the dataclasses.md file, there was no specific explanation about frozen, so I added an explanation about the frozen setting in ConfigDict(config.py).

- code change
```
            # It's not recommended to define both, as the setting from the dataclass decorator will take priority.
            if config_wrapper.frozen:
                warn(
                    "Both 'dataclass' decorator and 'config.frozen' are defined. 'config.frozen' take priority.",
                    UserWarning,
                )
```

## Related issue number

fix #9850

## Checklist

* [ ] The pull request title is a good summary of the changes - it will be used in the changelog
* [ ] Unit tests for the changes exist
* [ ] 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**

please review: @sydney-runkle

Problem statement:
Dataclass ConfigDict frozen doesn't work as expected
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

There are two apparent ways to make a dataclass frozen. Coming from using ordinary class-based Pydantic models to dataclass-based ones, I found myself providing ConfigDict(frozen=True) to the dataclass decorator. However, this doesn't work with dataclasses, instead you have to provide this argument to the decorator.

This could be made clearer in the docs, one of the arguments could be removed (pref the ConfigDict one), or the dataclass could default to either of which is set (would be a problem if both were set to opposite values).

Cheers!

### Example Code

```Python
from pydantic import ConfigDict
from pydantic.dataclasses import dataclass

# Throws error:

@dataclass(config=ConfigDict(frozen=True))
class Location:
    latitude: float
    longitude: float

location = Location(latitude=0, longitude=0)
hash(location)
# TypeError: unhashable type: 'Location'


# Works:

@dataclass(frozen=True)
class Location:
    latitude: float
    longitude: float

location = Location(latitude=0, longitude=0)
hash(location)
# -8458139203682520985
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.7.4
        pydantic-core version: 2.18.4
          pydantic-core build: profile=release pgo=true
                 install path: /Users/gregbrown/Development/carta/app/pipeline_service/.venv/lib/python3.12/site-packages/pydantic
               python version: 3.12.3 (main, Jun 11 2024, 09:22:06) [Clang 15.0.0 (clang-1500.3.9.4)]
                     platform: macOS-14.5-arm64-arm-64bit
             related packages: mypy-1.10.0 pydantic-settings-2.3.4 typing_extensions-4.12.2
                       commit: unknown
```

Hints from issue discussion:
A docs update would be awesome, thanks!
Hi @sydney-runkle,
I'd like to work on this. Can I work on this?


Hi @sydney-runkle,
Thank you for assigning me!

I would like to discuss a policy for revising the documentation.
Currently, after reading the docs, I'm considering proposing the following policy:

- Defining both `@dataclasses.dataclass(frozen=True)` and `ConfigDict(frozen=True)` in the same model definition should be avoided, as it may lead to unexpected behavior or errors.
- There are no particular priorities between these two options.
- Therefore, I want to add the following wording to the dataclasses.md file:

!!! note
    Pydantic should not support using `@dataclasses.dataclass(frozen=True)` and `ConfigDict(frozen=True)` together in the same model definition. Models can be configured to be immutable via `@dataclasses.dataclass(frozen=True)` or `Config(frozen=True)` (not `ConfigDict(frozen=True)`).


Could you please review the proposed policy and let me know if there are any issues with it?
Hmm. Reading through this again, I think we should probably warn if `frozen` is defined in both places, but give priority to the `frozen` from the `dataclass` constructor.

In the docs, we can specify: it's not recommended to define both, as the setting from the `dataclass` decorator will take priority.

This will require a minor code update (including tests) + a docs update as well. It's a great first issue! I'm guessing all code changes can be made in `dataclasses.py` and `_internal/_pydantic_dataclasses.py`.

Hope this helps - let me know if you have any follow up questions!
Hi @sydney-runkle,
Thank you for the guidance!

I want to confirm my understanding:

Documentation Update:
- I will update the dataclasses.md file and add the following line:
It's not recommended to define both, as the setting from the `dataclass` decorator will take priority.

Code Changes:
- When both dataclass decorator and __frozen__ are defined, the dataclass setting will take priority.
- A warning will be raised if frozen is defined in both locations.
- I will add tests to cover the case where frozen is defined in both locations.

The code changes will likely be made in dataclasses.py and _internal/_pydantic_dataclasses.py.

Please let me know if I have understood the requirements correctly or if you have any other feedback.
Thank you again for the opportunity to contribute!
> Please let me know if I have understood the requirements correctly or if you have any other feedback.
Thank you again for the opportunity to contribute!

Sure thing. You got it all right! Super excited to have you contribute - feel free to ping me with any follow up questions / when you want a review!
@mochi22,

I've started to implement some of the code changes in the above PR, though I haven't added the warning or docs update yet - I'll leave that to you!

Commit message: Delete test file

Patch to review:
diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py
index 2467493dbe5..51d3cba957b 100644
--- a/pydantic/dataclasses.py
+++ b/pydantic/dataclasses.py
@@ -6,6 +6,7 @@
 import sys
 import types
 from typing import TYPE_CHECKING, Any, Callable, Generic, NoReturn, TypeVar, overload
+from warnings import warn
 
 from typing_extensions import Literal, TypeGuard, dataclass_transform
 
@@ -124,7 +125,7 @@ def dataclass(
         order: Determines if comparison magic methods should be generated, such as `__lt__`, but not `__eq__`.
         unsafe_hash: Determines if a `__hash__` method should be included in the class, as in `dataclasses.dataclass`.
         frozen: Determines if the generated class should be a 'frozen' `dataclass`, which does not allow its
-            attributes to be modified after it has been initialized.
+            attributes to be modified after it has been initialized. If not set, the value from the provided `config` argument will be used (and will default to `False` otherwise).
         config: The Pydantic config to use for the `dataclass`.
         validate_on_init: A deprecated parameter included for backwards compatibility; in V2, all Pydantic dataclasses
             are validated on init.
@@ -232,6 +233,13 @@ def create_dataclass(cls: type[Any]) -> type[PydanticDataclass]:
         else:
             frozen_ = config_wrapper.frozen or False
 
+        # It's not recommended to define both, as the setting from the dataclass decorator will take priority.
+        if frozen is not None and config_wrapper.frozen == True:
+            warn(
+                "Both 'dataclass' decorator and 'config.frozen' are defined. 'config.frozen' take priority.",
+                UserWarning,
+            )
+
         cls = dataclasses.dataclass(  # type: ignore[call-overload]
             cls,
             # the value of init here doesn't affect anything except that it makes it easier to generate a signature


Code review comments:
diff_hunk: @@ -232,6 +233,13 @@ def create_dataclass(cls: type[Any]) -> type[PydanticDataclass]:
         else:
             frozen_ = config_wrapper.frozen or False
 
+        # It's not recommended to define both, as the setting from the dataclass decorator will take priority.
+        if frozen is not None and config_wrapper.frozen == True:
+            warn(
+                "Both 'dataclass' decorator and 'config.frozen' are defined. 'config.frozen' take priority.",
+                UserWarning,
+            )
path: pydantic/dataclasses.py
text: @user1:
```suggestion
if frozen is not None:
    frozen_ = frozen
    if config_wrapper.frozen:
        # It's not recommended to define both, as the setting from the dataclass decorator will take priority.
        warn(...)
else:
    frozen_ = config_wrapper.frozen or False
```

Maybe?

@author:
Thank you for your comments, and I agree with you! 
I have fixed it