Important
This PEP is a historical document: see@override and@typing.override for up-to-date specs and documentation. Canonical typing specs are maintained at thetyping specs site; runtime typing behaviour is described in the CPython documentation.
×
See thetyping specification update process for how to propose changes to the typing spec.
This PEP proposes adding an@override decorator to the Python type system.This will allow type checkers to prevent a class of bugs that occur when a baseclass changes methods that are inherited by derived classes.
A primary purpose of type checkers is to flag when refactors or changes breakpre-existing semantic structures in the code, so users can identify and makefixes across their project without doing a manual audit of their code.
Python’s type system does not provide a way to identify call sites that need tobe changed to stay consistent when an overridden function API changes. Thismakes refactoring and transforming code more dangerous.
Consider this simple inheritance structure:
classParent:deffoo(self,x:int)->int:returnxclassChild(Parent):deffoo(self,x:int)->int:returnx+1defparent_callsite(parent:Parent)->None:parent.foo(1)defchild_callsite(child:Child)->None:child.foo(1)
If the overridden method on the superclass is renamed or deleted, type checkerswill only alert us to update call sites that deal with the base type directly.But the type checker can only see the new code, not the change we made, so ithas no way of knowing that we probably also needed to rename the same method onchild classes.
A type checker will happily accept this code, even though we are likelyintroducing bugs:
classParent:# Rename this methoddefnew_foo(self,x:int)->int:returnxclassChild(Parent):# This (unchanged) method used to override `foo` but is unrelated to `new_foo`deffoo(self,x:int)->int:returnx+1defparent_callsite(parent:Parent)->None:# If we pass a Child instance we’ll now run Parent.new_foo - likely a bugparent.new_foo(1)defchild_callsite(child:Child)->None:# We probably wanted to invoke new_foo here. Instead, we forked the methodchild.foo(1)
This code will type check, but there are two potential sources of bugs:
Child instance to theparent_callsite function, it willinvoke the implementation inParent.new_foo. rather thanChild.foo.This is probably a bug - we presumably would not have writtenChild.foo inthe first place if we didn’t need custom behavior.Child.foo behaving in a similar way toParent.foo. But unless we catch this early, we have now forked themethods, and in future refactors it is likely no one will realize that majorchanges to the behavior ofnew_foo likely require updatingChild.foo aswell, which could lead to major bugs later.The incorrectly-refactored code is type-safe, but is probably not what weintended and could cause our system to behave incorrectly. The bug can bedifficult to track down because our new code likely does execute withoutthrowing exceptions. Tests are less likely to catch the problem, and silenterrors can take longer to track down in production.
We are aware of several production outages in multiple typed codebases caused bysuch incorrect refactors. This is our primary motivation for adding an@overridedecorator to the type system, which lets developers express the relationshipbetweenParent.foo andChild.foo so that type checkers can detect the problem.
We believe that explicit overrides will make unfamiliar code easier to read thanimplicit overrides. A developer reading the implementation of a subclass thatuses@override can immediately see which methods are overridingfunctionality in some base class; without this decorator, the only way toquickly find out is using a static analysis tool.
Many popular programming languages support override checks. For example:
override.override.<<__Override>>.@Override.override.override.override.override.Today, there is anOverrides librarythat provides decorators@overrides [sic] and@final and will enforcethem at runtime.
PEP 591 added a@final decorator with the same semantics as those in theOverrides library. But the override component of the runtime library is notsupported statically at all, which has added some confusion around themix/matched support.
Providing support for@override in static checks would add value because:
Using@override will make code more verbose.
When type checkers encounter a method decorated with@typing.override theyshould treat it as a type error unless that method is overriding a compatiblemethod or attribute in some ancestor class.
fromtypingimportoverrideclassParent:deffoo(self)->int:return1defbar(self,x:str)->str:returnxclassChild(Parent):@overridedeffoo(self)->int:return2@overridedefbaz(self)->int:# Type check error: no matching signature in ancestorreturn1
The@override decorator should be permitted anywhere a type checkerconsiders a method to be a valid override, which typically includes not onlynormal methods but also@property,@staticmethod, and@classmethod.
This PEP is exclusively concerned with the handling of the new@override decorator,which specifies that the decorated method must override some attribute inan ancestor class. This PEP does not propose any new rules regarding the typesignatures of such methods.
We believe that@override is most useful if checkers also allow developersto opt into a strict mode where methods that override a parent class arerequired to use the decorator. Strict enforcement should be opt-in for backwardcompatibility.
The primary reason for a strict mode that requires@override is that developerscan only trust that refactors are override-safe if they know that the@overridedecorator is used throughout the project.
There is another class of bug related to overrides that we can only catch using a strict mode.
Consider the following code:
classParent:passclassChild(Parent):deffoo(self)->int:return2
Imagine we refactor it as follows:
classParent:deffoo(self)->int:# This method is newreturn1classChild(Parent):deffoo(self)->int:# This is now an override!return2defcall_foo(parent:Parent)->int:returnparent.foo()# This could invoke Child.foo, which may be surprising.
The semantics of our code changed here, which could cause two problems:
Child.foo alreadyexisted (which is very possible in a large codebase), they might be surprisedto see thatcall_foo does not always invokeParent.foo.@override everywhere whenwriting overrides in subclasses, they are likely to miss the fact thatChild.foo needs it here.At first glance this kind of change may seem unlikely, but it can actuallyhappen often if one or more subclasses have functionality that developers laterrealize belongs in the base class.
With a strict mode, we will always alert developers when this occurs.
Most of the typed, object-oriented programming languages we looked at have aneasy way to require explicit overrides throughout a project:
By default, the@override decorator will be opt-in. Codebases that do notuse it will type-check as before, without the additional type safety.
__override__=True when possibleAt runtime,@typing.override will make a best-effort attempt to add anattribute__override__ with valueTrue to its argument. By “best-effort”we mean that we will try adding the attribute, but if that fails (for examplebecause the input is a descriptor type with fixed slots) we will silentlyreturn the argument as-is.
This is exactly what the@typing.final decorator does, and the motivationis similar: it gives runtime libraries the ability to use@override. As aconcrete example, a runtime library could check__override__ in orderto automatically populate the__doc__ attribute of child class methodsusing the parent method docstring.
__override__As described above, adding__override__ may fail at runtime, in whichcase we will simply return the argument as-is.
In addition, even in cases where it does work, it may be difficult for users tocorrectly work with multiple decorators, because successfully ensuring the__override__ attribute is set on the final output requires understanding theimplementation of each decorator:
@override decorator needs to executeafter ordinary decoratorslike@functools.lru_cache that use wrapper functions, since we want toset__override__ on the outermost wrapper. This means it needs togoabove all these other decorators.@override needs to executebefore many special descriptor-baseddecorators like@property,@staticmethod, and@classmethod.__override__ attribute at all.As a result, runtime support for setting__override__ is best effortonly, and we do not expect type checkers to validate the ordering ofdecorators.
Modern Integrated Development Environments (IDEs) often provide the ability toautomatically update subclasses when renaming a method. But we view this asinsufficient for several reasons:
We considered having@typing.override enforce override safety at runtime,similarly to how@overrides.overridesdoes today.
We rejected this for four reasons:
@overrides.overrides implementation takes around 100 microseconds, whichis fast but could still add up to a second or more of extra initializationtime in million-plus line codebases, which is exactly where we think@typing.override will be most useful.@overrides.overridesdoes) or to use a metaclass-based approach. Neither approach seems ideal.We considered including a class decorator@require_explicit_overrides, whichwould have provided a way for base classes to declare that all subclasses mustuse the@override decorator on method overrides. TheOverrides library has a mixin class,EnforceExplicitOverrides, which provides similar behavior in runtime checks.
We decided against this because we expect owners of large codebases will benefitmost from@override, and for these use cases having a strict mode whereexplicit@override is required (see the Backward Compatibility section)provides more benefits than a way to mark base classes.
Moreover we believe that authors of projects who do not consider the extra typesafety to be worth the additional boilerplate of using@override should notbe forced to do so. Having an optional strict mode puts the decision in thehands of project owners, whereas the use of@require_explicit_overrides inlibraries would force project owners to use@override even if they prefernot to.
We considered allowing the caller of@override to specify a specificancestor class where the overridden method should be defined:
classParent0:deffoo(self)->int:return1classParent1:defbar(self)->int:return1classChild(Parent0,Parent1):@override(Parent0)# okay, Parent0 defines foodeffoo(self)->int:return2@override(Parent0)# type error, Parent0 does not define bardefbar(self)->int:return2
This could be useful for code readability because it makes the overridestructure more explicit for deep inheritance trees. It also might catch bugs byprompting developers to check that the implementation of an override still makessense whenever a method being overridden moves from one base class to another.
We decided against it because:
@override and type checker support for it, so there would need tobe considerable benefits.Pyre: A proof of concept is implemented in Pyre:
This document is placed in the public domain or under theCC0-1.0-Universal license, whichever is more permissive.
Source:https://github.com/python/peps/blob/main/peps/pep-0698.rst
Last modified:2024-06-11 22:12:09 GMT