Skip to content
10 changes: 9 additions & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3657,7 +3657,11 @@ def unwrap_final(self, s: AssignmentStmt) -> bool:
else:
s.type = s.unanalyzed_type.args[0]

if s.type is not None and self.is_classvar(s.type):
if (
s.type is not None
and self.options.python_version < (3, 13)
and self.is_classvar(s.type)
):
self.fail("Variable should not be annotated with both ClassVar and Final", s)
return False

Expand Down Expand Up @@ -7357,6 +7361,7 @@ def type_analyzer(
allow_unbound_tvars: bool = False,
allow_placeholder: bool = False,
allow_typed_dict_special_forms: bool = False,
allow_final: bool = False,
allow_param_spec_literals: bool = False,
allow_unpack: bool = False,
report_invalid_types: bool = True,
Expand All @@ -7378,6 +7383,7 @@ def type_analyzer(
report_invalid_types=report_invalid_types,
allow_placeholder=allow_placeholder,
allow_typed_dict_special_forms=allow_typed_dict_special_forms,
allow_final=allow_final,
allow_param_spec_literals=allow_param_spec_literals,
allow_unpack=allow_unpack,
prohibit_self_type=prohibit_self_type,
Expand All @@ -7402,6 +7408,7 @@ def anal_type(
allow_unbound_tvars: bool = False,
allow_placeholder: bool = False,
allow_typed_dict_special_forms: bool = False,
allow_final: bool = False,
allow_param_spec_literals: bool = False,
allow_unpack: bool = False,
report_invalid_types: bool = True,
Expand Down Expand Up @@ -7438,6 +7445,7 @@ def anal_type(
allow_tuple_literal=allow_tuple_literal,
allow_placeholder=allow_placeholder,
allow_typed_dict_special_forms=allow_typed_dict_special_forms,
allow_final=allow_final,
allow_param_spec_literals=allow_param_spec_literals,
allow_unpack=allow_unpack,
report_invalid_types=report_invalid_types,
Expand Down
18 changes: 12 additions & 6 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ def __init__(
allow_unbound_tvars: bool = False,
allow_placeholder: bool = False,
allow_typed_dict_special_forms: bool = False,
allow_final: bool = True,
allow_param_spec_literals: bool = False,
allow_unpack: bool = False,
report_invalid_types: bool = True,
Expand Down Expand Up @@ -260,6 +261,8 @@ def __init__(
self.allow_placeholder = allow_placeholder
# Are we in a context where Required[] is allowed?
self.allow_typed_dict_special_forms = allow_typed_dict_special_forms
# Set True when we analyze ClassVar else False
self.allow_final = allow_final
# Are we in a context where ParamSpec literals are allowed?
self.allow_param_spec_literals = allow_param_spec_literals
# Are we in context where literal "..." specifically is allowed?
Expand Down Expand Up @@ -606,11 +609,12 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ
code=codes.VALID_TYPE,
)
else:
self.fail(
"Final can be only used as an outermost qualifier in a variable annotation",
t,
code=codes.VALID_TYPE,
)
if not self.allow_final:
self.fail(
"Final can be only used as an outermost qualifier in a variable annotation",
t,
code=codes.VALID_TYPE,
)
return AnyType(TypeOfAny.from_error)
elif fullname == "typing.Tuple" or (
fullname == "builtins.tuple"
Expand Down Expand Up @@ -691,7 +695,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ
"ClassVar[...] must have at most one type argument", t, code=codes.VALID_TYPE
)
return AnyType(TypeOfAny.from_error)
return self.anal_type(t.args[0])
return self.anal_type(t.args[0], allow_final=self.options.python_version >= (3, 13))
elif fullname in NEVER_NAMES:
return UninhabitedType()
elif fullname in LITERAL_TYPE_NAMES:
Expand Down Expand Up @@ -1877,11 +1881,13 @@ def anal_type(
allow_unpack: bool = False,
allow_ellipsis: bool = False,
allow_typed_dict_special_forms: bool = False,
allow_final: bool = False,
) -> Type:
if nested:
self.nesting_level += 1
old_allow_typed_dict_special_forms = self.allow_typed_dict_special_forms
self.allow_typed_dict_special_forms = allow_typed_dict_special_forms
self.allow_final = allow_final
old_allow_ellipsis = self.allow_ellipsis
self.allow_ellipsis = allow_ellipsis
old_allow_unpack = self.allow_unpack
Expand Down
11 changes: 11 additions & 0 deletions test-data/unit/check-final.test
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ def g(x: int) -> Final[int]: ... # E: Final can be only used as an outermost qu
[out]

[case testFinalDefiningNotInMethodExtensions]
# flags: --python-version 3.14
from typing_extensions import Final

def f(x: Final[int]) -> int: ... # E: Final can be only used as an outermost qualifier in a variable annotation
Expand Down Expand Up @@ -1128,6 +1129,7 @@ class A:
[builtins fixtures/tuple.pyi]

[case testFinalUsedWithClassVar]
# flags: --python-version 3.12
from typing import Final, ClassVar

class A:
Expand All @@ -1136,6 +1138,15 @@ class A:
c: ClassVar[Final] = 1 # E: Final can be only used as an outermost qualifier in a variable annotation
[out]

[case testFinalUsedWithClassVarAfterPy313]
# flags: --python-version 3.13
from typing import Final, ClassVar

class A:
a: Final[ClassVar[int]] = 1
b: ClassVar[Final[int]] = 1
c: ClassVar[Final] = 1

[case testFinalClassWithAbstractMethod]
from typing import final
from abc import ABC, abstractmethod
Expand Down