@@ -292,20 +292,31 @@ class ChildOfBad(Bad):
292292
293293## Edge case: the function is decorated with ` @final ` but originally defined elsewhere
294294
295- As of 2025-11-26, pyrefly emits a diagnostic on this, but mypy and pyright do not:
295+ As of 2025-11-26, pyrefly emits a diagnostic on this, but mypy and pyright do not. For mypy and
296+ pyright to emit a diagnostic, the superclass definition decorated with ` @final ` must be a literal
297+ function definition: an assignment definition where the right-hand side of the assignment is a
298+ ` @final-decorated ` function is not sufficient for them to consider the superclass definition as
299+ being ` @final ` .
300+
301+ For now, we choose to follow mypy's and pyright's behaviour here, in order to maximise compatibility
302+ with other type checkers. We may decide to change this in the future, however, as it would simplify
303+ our implementation. Mypy's and pyright's behaviour here is also arguably inconsistent with their
304+ treatment of other type qualifiers such as ` Final ` . As discussed in
305+ < https://discuss.python.org/t/imported-final-variable/82429 > , both type checkers view the ` Final `
306+ type qualifier as travelling * across* scopes.
296307
297308``` py
298309from typing import final
299310
300311class A :
301312 @final
302- def method (self ): ...
313+ def method (self ) -> None : ...
303314
304315class B :
305316 method = A.method
306317
307318class C (B ):
308- def method (self ): ... # no diagnostic
319+ def method (self ) -> None : ... # no diagnostic here (see prose discussion above)
309320```
310321
311322## Constructor methods are also checked
@@ -378,3 +389,77 @@ class D(B): # error: [subclass-of-final-class]
378389 # TODO : we should emit a diagnostic here
379390 def method (self ): ...
380391```
392+
393+ ## An ` @final ` method is overridden by an implicit instance attribute
394+
395+ ``` py
396+ from typing import final, Any
397+
398+ class Parent :
399+ @final
400+ def method (self ) -> None : ...
401+
402+ class Child (Parent ):
403+ def __init__ (self ) -> None :
404+ self .method: Any = 42 # TODO : we should emit `[override-of-final-method]` here
405+ ```
406+
407+ ## A possibly-undefined ` @final ` method is overridden
408+
409+ <!-- snapshot-diagnostics -->
410+
411+ ``` py
412+ from typing import final
413+
414+ def coinflip () -> bool :
415+ return False
416+
417+ class A :
418+ if coinflip():
419+ @final
420+ def method1 (self ) -> None : ...
421+ else :
422+ def method1 (self ) -> None : ...
423+
424+ if coinflip():
425+ def method2 (self ) -> None : ...
426+ else :
427+ @final
428+ def method2 (self ) -> None : ...
429+
430+ if coinflip():
431+ @final
432+ def method3 (self ) -> None : ...
433+ else :
434+ @final
435+ def method3 (self ) -> None : ...
436+
437+ if coinflip():
438+ def method4 (self ) -> None : ...
439+ elif coinflip():
440+ @final
441+ def method4 (self ) -> None : ...
442+ else :
443+ def method4 (self ) -> None : ...
444+
445+ class B (A ):
446+ def method1 (self ) -> None : ... # error: [override-of-final-method]
447+ def method2 (self ) -> None : ... # error: [override-of-final-method]
448+ def method3 (self ) -> None : ... # error: [override-of-final-method]
449+ def method4 (self ) -> None : ... # error: [override-of-final-method]
450+
451+ # Possible overrides of possibly `@final` methods...
452+ class C (A ):
453+ if coinflip():
454+ # TODO : the autofix here introduces invalid syntax because there are now no
455+ # statements inside the `if:` branch
456+ # (but it might still be a useful autofix in an IDE context?)
457+ def method1 (self ) -> None : ... # error: [override-of-final-method]
458+ else :
459+ pass
460+
461+ if coinflip():
462+ def method2 (self ) -> None : ... # error: [override-of-final-method]
463+ def method3 (self ) -> None : ... # error: [override-of-final-method]
464+ def method4 (self ) -> None : ... # error: [override-of-final-method]
465+ ```
0 commit comments