Repository: pylint-dev/pylint

Issue:
# Pylint C0103 (invalid-name) for dataclass Final field

### Bug description

A special case of https://github.com/pylint-dev/pylint/issues/10711.
According to https://github.com/python/typing/pull/1669, and Python 3.13 `Final can now be nested in ClassVar and vice versa.`

```python
from dataclasses import dataclass, field
from typing import ClassVar, Final


@dataclass
class MyClass:
    CLASS_CONSTANT: ClassVar[Final] = 42

    instance_constant: Final[int]  # pylint treats as class constant
    instance_constant_with_default: Final[int] = 42  # pylint treats as class constant
    instance_constant_with_field: Final[int] = field(default=42)  # pylint treats as class constant

```

pyright behaves differently from pylint. [pyright](https://pyright-play.net/?strict=true&enableExperimentalFeatures=true&code=GYJw9gtgBAJghgFzgYwDZwM4YKYagSwgAcwQFZEV0sAaKYfbVGAKFEigQE8j8A7AOYFipcgGFqGAGpwQdAGL84qFqoAC8JGkwYW2rFACyXCToBcLKFahiAMgEEAyo4D6YgPIA5RwBV7nnzMbSRkQAG1FPmUAXSgAXigAFgAmS2sGJhgXfAwXPjAEF31c5DA%2BDAQgyOUw-gRYhJSoAGIoIi4QfAEAC3IEEGxEPEwKLUl6RmZVawJypD5kbCKyirg%2BSqhq1Fr12Ja2rlQ6zgGhqBHiqFK5tYQ0q35VhaXrp8KAd3wEbpcYbGA4ABXVAbLY7erxJLJKytdpHdYnQYIYZ4S6veZ3GaPeaLZY3dYuT7fFwZZhVJTbOoNCaZAAUfwBwIQcRSAEoYQd4X1TsjzqjxujbqoWEA)

### Configuration

```ini

```

### Command used

```shell
pylint a.py
```

### Pylint output

```python
************* Module a
a.py:9:4: C0103: Class constant name "instance_constant" doesn't conform to UPPER_CASE naming style (invalid-name)
a.py:10:4: C0103: Class constant name "instance_constant_with_default" doesn't conform to UPPER_CASE naming style (invalid-name)
a.py:11:4: C0103: Class constant name "instance_constant_with_field" doesn't conform to UPPER_CASE naming style (invalid-name)
```

### Expected behavior

For dataclass, only `ClassVar[Final] ` variable should be treat as Class constant.

### Pylint version

```shell
pylint 4.0.4
astroid 4.0.2
Python 3.13.7 (main, Sep 18 2025, 19:43:45) [MSC v.1944 64 bit (AMD64)]
```

### OS / Environment

_No response_

### Additional dependencies

```python

```

Implementation patch:
diff --git a/pylint/checkers/base/name_checker/checker.py b/pylint/checkers/base/name_checker/checker.py
index 9b3ed0ed1f..41ccc674a1 100644
--- a/pylint/checkers/base/name_checker/checker.py
+++ b/pylint/checkers/base/name_checker/checker.py
@@ -552,9 +552,14 @@ def visit_assignname(  # pylint: disable=too-many-branches,too-many-statements
         elif isinstance(frame, nodes.ClassDef) and not any(
             frame.local_attr_ancestors(node.name)
         ):
-            if utils.is_enum_member(node) or utils.is_assign_name_annotated_with(
-                node, "Final"
-            ):
+            if utils.is_assign_name_annotated_with_class_var_typing_name(node, "Final"):
+                self._check_name("class_const", node.name, node)
+            elif utils.is_assign_name_annotated_with(node, "Final"):
+                if frame.is_dataclass:
+                    self._check_name("class_attribute", node.name, node)
+                else:
+                    self._check_name("class_const", node.name, node)
+            elif utils.is_enum_member(node):
                 self._check_name("class_const", node.name, node)
             else:
                 self._check_name("class_attribute", node.name, node)
diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py
index 5c44a8256e..4a17d53525 100644
--- a/pylint/checkers/utils.py
+++ b/pylint/checkers/utils.py
@@ -1762,6 +1762,22 @@ def is_assign_name_annotated_with(node: nodes.AssignName, typing_name: str) -> b
     return False
 
 
+def is_assign_name_annotated_with_class_var_typing_name(
+    node: nodes.AssignName, typing_name: str
+) -> bool:
+    if not is_assign_name_annotated_with(node, "ClassVar"):
+        return False
+    annotation = node.parent.annotation
+    if isinstance(annotation, nodes.Subscript):
+        annotation = annotation.slice
+        if isinstance(annotation, nodes.Subscript):
+            annotation = annotation.value
+    match annotation:
+        case nodes.Name(name=n) | nodes.Attribute(attrname=n) if n == typing_name:
+            return True
+    return False
+
+
 def get_iterating_dictionary_name(node: nodes.For | nodes.Comprehension) -> str | None:
     """Get the name of the dictionary which keys are being iterated over on
     a ``nodes.For`` or ``nodes.Comprehension`` node.


Test patch:
diff --git a/tests/functional/i/invalid/invalid_name/invalid_name_with_final_typing.py b/tests/functional/i/invalid/invalid_name/invalid_name_with_final_typing.py
index 3d32d8a2b4..f0f28687f2 100644
--- a/tests/functional/i/invalid/invalid_name/invalid_name_with_final_typing.py
+++ b/tests/functional/i/invalid/invalid_name/invalid_name_with_final_typing.py
@@ -3,7 +3,7 @@
 # pylint: disable=missing-class-docstring, missing-function-docstring
 
 from dataclasses import dataclass
-from typing import Final
+from typing import ClassVar, Final
 
 module_snake_case_constant: Final[int] = 42  # [invalid-name]
 MODULE_UPPER_CASE_CONSTANT: Final[int] = 42
@@ -17,8 +17,11 @@ def function() -> None:
 
 @dataclass
 class Class:
-    class_snake_case_constant: Final[int] = 42  # [invalid-name]
-    CLASS_UPPER_CASE_CONSTANT: Final[int] = 42
+    class_snake_case_constant: ClassVar[Final[int]] = 42  # [invalid-name]
+    CLASS_UPPER_CASE_CONSTANT: ClassVar[Final[int]] = 42
+
+    field_annotated_with_final: Final[int] = 42
+    FIELD_ANNOTATED_WITH_FINAL: Final[int] = 42
 
     def method(self) -> None:
         method_snake_case_constant: Final[int] = 42