13 KiB
Tests for the @typing(_extensions).final decorator
Cannot subclass a class decorated with @final
Don't do this:
import typing_extensions
from typing import final
@final
class A: ...
class B(A): ... # error: 9 [subclass-of-final-class] "Class `B` cannot inherit from final class `A`"
@typing_extensions.final
class C: ...
class D(C): ... # error: [subclass-of-final-class]
class E: ...
class F: ...
class G: ...
# fmt: off
class H(
E,
F,
A, # error: [subclass-of-final-class]
G,
): ...
Cannot override a method decorated with @final
from typing_extensions import final, Callable, TypeVar
def lossy_decorator(fn: Callable) -> Callable: ...
class Parent:
@final
def foo(self): ...
@final
@property
def my_property1(self) -> int: ...
@property
@final
def my_property2(self) -> int: ...
@property
@final
def my_property3(self) -> int: ...
@final
@classmethod
def class_method1(cls) -> int: ...
@classmethod
@final
def class_method2(cls) -> int: ...
@final
@staticmethod
def static_method1() -> int: ...
@staticmethod
@final
def static_method2() -> int: ...
@lossy_decorator
@final
def decorated_1(self): ...
@final
@lossy_decorator
def decorated_2(self): ...
class Child(Parent):
# explicitly test the concise diagnostic message,
# which is different to the verbose diagnostic summary message:
#
# error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`"
def foo(self): ...
@property
def my_property1(self) -> int: ... # error: [override-of-final-method]
@property
def my_property2(self) -> int: ... # error: [override-of-final-method]
@my_property2.setter
def my_property2(self, x: int) -> None: ...
@property
def my_property3(self) -> int: ... # error: [override-of-final-method]
@my_property3.deleter
def my_proeprty3(self) -> None: ...
@classmethod
def class_method1(cls) -> int: ... # error: [override-of-final-method]
@staticmethod
def static_method1() -> int: ... # error: [override-of-final-method]
@classmethod
def class_method2(cls) -> int: ... # error: [override-of-final-method]
@staticmethod
def static_method2() -> int: ... # error: [override-of-final-method]
def decorated_1(self): ... # TODO: should emit [override-of-final-method]
@lossy_decorator
def decorated_2(self): ... # TODO: should emit [override-of-final-method]
class OtherChild(Parent): ...
class Grandchild(OtherChild):
@staticmethod
# TODO: we should emit a Liskov violation here too
# error: [override-of-final-method]
def foo(): ...
@property
# TODO: we should emit a Liskov violation here too
# error: [override-of-final-method]
def my_property1(self) -> str: ...
# TODO: we should emit a Liskov violation here too
# error: [override-of-final-method]
class_method1 = None
# Diagnostic edge case: `final` is very far away from the method definition in the source code:
T = TypeVar("T")
def identity(x: T) -> T: ...
class Foo:
@final
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
def bar(self): ...
class Baz(Foo):
def bar(self): ... # error: [override-of-final-method]
Diagnostic edge case: superclass with @final method has the same name as the subclass
module1.py:
from typing import final
class Foo:
@final
def f(self): ...
module2.py:
import module1
class Foo(module1.Foo):
def f(self): ... # error: [override-of-final-method]
Overloaded methods decorated with @final
In a stub file, @final should be applied to the first overload. In a runtime file, @final should
only be applied to the implementation function.
stub.pyi:
from typing import final, overload
class Good:
@overload
@final
def bar(self, x: str) -> str: ...
@overload
def bar(self, x: int) -> int: ...
@final
@overload
def baz(self, x: str) -> str: ...
@overload
def baz(self, x: int) -> int: ...
class ChildOfGood(Good):
@overload
def bar(self, x: str) -> str: ...
@overload
def bar(self, x: int) -> int: ... # error: [override-of-final-method]
@overload
def baz(self, x: str) -> str: ...
@overload
def baz(self, x: int) -> int: ... # error: [override-of-final-method]
class Bad:
@overload
def bar(self, x: str) -> str: ...
@overload
@final
# error: [invalid-overload]
def bar(self, x: int) -> int: ...
@overload
def baz(self, x: str) -> str: ...
@final
@overload
# error: [invalid-overload]
def baz(self, x: int) -> int: ...
class ChildOfBad(Bad):
@overload
def bar(self, x: str) -> str: ...
@overload
def bar(self, x: int) -> int: ... # error: [override-of-final-method]
@overload
def baz(self, x: str) -> str: ...
@overload
def baz(self, x: int) -> int: ... # error: [override-of-final-method]
main.py:
from typing import overload, final
class Good:
@overload
def f(self, x: str) -> str: ...
@overload
def f(self, x: int) -> int: ...
@final
def f(self, x: int | str) -> int | str:
return x
class ChildOfGood(Good):
@overload
def f(self, x: str) -> str: ...
@overload
def f(self, x: int) -> int: ...
# error: [override-of-final-method]
def f(self, x: int | str) -> int | str:
return x
class Bad:
@overload
@final
def f(self, x: str) -> str: ...
@overload
def f(self, x: int) -> int: ...
# error: [invalid-overload]
def f(self, x: int | str) -> int | str:
return x
@final
@overload
def g(self, x: str) -> str: ...
@overload
def g(self, x: int) -> int: ...
# error: [invalid-overload]
def g(self, x: int | str) -> int | str:
return x
@overload
def h(self, x: str) -> str: ...
@overload
@final
def h(self, x: int) -> int: ...
# error: [invalid-overload]
def h(self, x: int | str) -> int | str:
return x
@overload
def i(self, x: str) -> str: ...
@final
@overload
def i(self, x: int) -> int: ...
# error: [invalid-overload]
def i(self, x: int | str) -> int | str:
return x
class ChildOfBad(Bad):
# TODO: these should all cause us to emit Liskov violations as well
f = None # error: [override-of-final-method]
g = None # error: [override-of-final-method]
h = None # error: [override-of-final-method]
i = None # error: [override-of-final-method]
Edge case: the function is decorated with @final but originally defined elsewhere
As of 2025-11-26, pyrefly emits a diagnostic on this, but mypy and pyright do not. For mypy and
pyright to emit a diagnostic, the superclass definition decorated with @final must be a literal
function definition: an assignment definition where the right-hand side of the assignment is a
@final-decorated function is not sufficient for them to consider the superclass definition as
being @final.
For now, we choose to follow mypy's and pyright's behaviour here, in order to maximise compatibility
with other type checkers. We may decide to change this in the future, however, as it would simplify
our implementation. Mypy's and pyright's behaviour here is also arguably inconsistent with their
treatment of other type qualifiers such as Final. As discussed in
https://discuss.python.org/t/imported-final-variable/82429, both type checkers view the Final
type qualifier as travelling across scopes.
from typing import final
class A:
@final
def method(self) -> None: ...
class B:
method = A.method
class C(B):
def method(self) -> None: ... # no diagnostic here (see prose discussion above)
Constructor methods are also checked
from typing import final
class A:
@final
def __init__(self) -> None: ...
class B(A):
def __init__(self) -> None: ... # error: [override-of-final-method]
Only the first @final violation is reported
(Don't do this.)
from typing import final
class A:
@final
def f(self): ...
class B(A):
@final
def f(self): ... # error: [override-of-final-method]
class C(B):
@final
# we only emit one error here, not two
def f(self): ... # error: [override-of-final-method]
For when you just really want to drive the point home
from typing import final, Final
@final
@final
@final
@final
@final
@final
class A:
@final
@final
@final
@final
@final
def method(self): ...
@final
@final
@final
@final
@final
class B:
method: Final = A.method
class C(A): # error: [subclass-of-final-class]
def method(self): ... # error: [override-of-final-method]
class D(B): # error: [subclass-of-final-class]
# TODO: we should emit a diagnostic here
def method(self): ...
An @final method is overridden by an implicit instance attribute
from typing import final, Any
class Parent:
@final
def method(self) -> None: ...
class Child(Parent):
def __init__(self) -> None:
self.method: Any = 42 # TODO: we should emit `[override-of-final-method]` here
A possibly-undefined @final method is overridden
from typing import final
def coinflip() -> bool:
return False
class A:
if coinflip():
@final
def method1(self) -> None: ...
else:
def method1(self) -> None: ...
if coinflip():
def method2(self) -> None: ...
else:
@final
def method2(self) -> None: ...
if coinflip():
@final
def method3(self) -> None: ...
else:
@final
def method3(self) -> None: ...
if coinflip():
def method4(self) -> None: ...
elif coinflip():
@final
def method4(self) -> None: ...
else:
def method4(self) -> None: ...
class B(A):
def method1(self) -> None: ... # error: [override-of-final-method]
def method2(self) -> None: ... # error: [override-of-final-method]
def method3(self) -> None: ... # error: [override-of-final-method]
# check that autofixes don't introduce invalid syntax
# if there are multiple statements on one line
#
# TODO: we should emit a Liskov violation here too
# error: [override-of-final-method]
method4 = 42; unrelated = 56 # fmt: skip
# Possible overrides of possibly `@final` methods...
class C(A):
if coinflip():
def method1(self) -> None: ... # error: [override-of-final-method]
else:
pass
if coinflip():
def method2(self) -> None: ... # error: [override-of-final-method]
else:
def method2(self) -> None: ...
if coinflip():
def method3(self) -> None: ... # error: [override-of-final-method]
# TODO: we should emit Liskov violations here too:
if coinflip():
method4 = 42 # error: [override-of-final-method]
else:
method4 = 56
Definitions in statically known branches
[environment]
python-version = "3.10"
import sys
from typing_extensions import final
class Parent:
if sys.version_info >= (3, 10):
@final
def foo(self) -> None: ...
@final
def foooo(self) -> None: ...
@final
def baaaaar(self) -> None: ...
else:
@final
def bar(self) -> None: ...
@final
def baz(self) -> None: ...
@final
def spam(self) -> None: ...
class Child(Parent):
def foo(self) -> None: ... # error: [override-of-final-method]
# The declaration on `Parent` is not reachable,
# so this is fine
def bar(self) -> None: ...
if sys.version_info >= (3, 10):
def foooo(self) -> None: ... # error: [override-of-final-method]
def baz(self) -> None: ...
else:
# Fine because this doesn't override any reachable definitions
def foooo(self) -> None: ...
# There are `@final` definitions being overridden here,
# but the definitions that override them are unreachable
def spam(self) -> None: ...
def baaaaar(self) -> None: ...
Overloads in statically-known branches in stub files
[environment]
python-version = "3.10"
import sys
from typing_extensions import overload, final
class Foo:
if sys.version_info >= (3, 10):
@overload
@final
def method(self, x: int) -> int: ...
else:
@overload
def method(self, x: int) -> int: ...
@overload
def method(self, x: str) -> str: ...
if sys.version_info >= (3, 10):
@overload
def method2(self, x: int) -> int: ...
else:
@overload
@final
def method2(self, x: int) -> int: ...
@overload
def method2(self, x: str) -> str: ...
class Bar(Foo):
@overload
def method(self, x: int) -> int: ...
@overload
def method(self, x: str) -> str: ... # error: [override-of-final-method]
# This is fine: the only overload that is marked `@final`
# is in a statically-unreachable branch
@overload
def method2(self, x: int) -> int: ...
@overload
def method2(self, x: str) -> str: ...