From 59696d106cf4dbb4d6ab93832cf3ff66b955b2b7 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Mon, 12 May 2025 22:44:37 +0200 Subject: [PATCH 1/3] Admit that Final variables are never redefined --- mypy/checker.py | 4 ++++ test-data/unit/check-final.test | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 2d82d74cc197e..758a860abf18a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1557,6 +1557,10 @@ def is_var_redefined_in_outer_context(self, v: Var, after_line: int) -> bool: Note that this doesn't do a full CFG analysis but uses a line number based heuristic that isn't correct in some (rare) cases. """ + if v.is_final: + # Final vars are definitely never reassigned. + return False + outers = self.tscope.outer_functions() if not outers: # Top-level function -- outer context is top level, and we can't reason about diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 4b0bab45d16ce..5189149d4743c 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -1250,3 +1250,30 @@ def check_final_init() -> None: new_instance = FinalInit() new_instance.__init__() [builtins fixtures/tuple.pyi] + +[case testNarrowingOfFinalPersistsInFunctions] +from typing import Final, Union + +def _init() -> Union[int, None]: + return 0 + +FOO: Final = _init() +# For some reason the following isn't narrowed at all +# (always stays int | None): +# FOO: Final[Union[int, None]] = 0 + +class Example: + + if FOO is not None: + reveal_type(FOO) # N: Revealed type is "builtins.int" + + def fn(self) -> int: + return FOO + +if FOO is not None: + reveal_type(FOO) # N: Revealed type is "builtins.int" + + def func() -> int: + return FOO + +[builtins fixtures/tuple.pyi] From 1a7599ec3ba1381c4ec585dcccfbefe23b83590e Mon Sep 17 00:00:00 2001 From: STerliakov Date: Mon, 12 May 2025 23:10:35 +0200 Subject: [PATCH 2/3] Remove unneeded fixture --- test-data/unit/check-final.test | 2 -- 1 file changed, 2 deletions(-) diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 5189149d4743c..d0c5b280400d0 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -1275,5 +1275,3 @@ if FOO is not None: def func() -> int: return FOO - -[builtins fixtures/tuple.pyi] From 8af453994fccc667558644feb9c8786e366c4bcc Mon Sep 17 00:00:00 2001 From: Stanislav Terliakov <50529348+sterliakov@users.noreply.github.com> Date: Tue, 13 May 2025 01:36:07 +0200 Subject: [PATCH 3/3] Remove comment on the known problem from the testcase Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com> --- test-data/unit/check-final.test | 3 --- 1 file changed, 3 deletions(-) diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index d0c5b280400d0..d78c2a8e57f2e 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -1258,9 +1258,6 @@ def _init() -> Union[int, None]: return 0 FOO: Final = _init() -# For some reason the following isn't narrowed at all -# (always stays int | None): -# FOO: Final[Union[int, None]] = 0 class Example: