typing.override decorator
Hi everyone! Pyre has received several user requests over the last few months to support @override functionality in the Python type system. I’ve compiled some of details here to see what you all think (I hope the formatting works over the mailing list): Motivation: One core functionality of the type checker is to flag when refactors or changes break pre-existing semantic structures in the code, so users can identify and make fixes across their project without doing a manual audit of their code. Unlike most other language type systems, Python does not provide a way to identify call sites that need to be changed to stay consistent when an overridden function API changes. This makes refactoring code dangerous and increases the cost of re-architectures and codemods. Take for example, a simple inheritance structure: class Base: def foo(self, input: int) -> int: return input class Child(Base): def foo(self, input: int) -> int: return input def callsite(input: Child) -> None: input.foo(1) If an overridden method on the superclass is renamed or deleted, the type checker will only identify and flag call sites that deal with objects of the base class directly, but still happily accept any call sites dealing with subclasses. class Base: def new_foo(self, input: int, flag: bool) -> int: # new semantics / side effects return input class Child(Base): # this method now silently does something fundamentally different - defines a new function unrelated to what it used to override def foo(self, input: int) -> int: return input def callsite(input: Child) -> None: input.foo(1) # no indication this API changed While this will run when executed, the type checker omits to mention the fact that a core piece of semantics have changed: the subclass method that was formerly overriding a superclass method is now, silently, defining a brand new method on the class and fooling call-sites into thinking nothing has changed with the old API. Explicitly marking a method as intending to override something in the parent would help us flag these problems in a complicated inheritance system. We’ve seen runtime bug reports usually caused by an engineer renaming a parent class method and forgetting to rename that method in all child classes. This blows up at runtime and causes some user frustration because this pattern is caught in type systems of many other languages (see below). Syntax & Semantics: A method decorated with @typing.override will throw a type error if it is not actively overriding a compatible method in a (non-Protocol) parent class. from typing import override class Base: def foo(self) -> int: return 1 def bar(self, x: str) -> str: return x class Child(Base): @override def foo(self) -> int: return 2 @override def bar(self, y: int) -> int: # type error: signature mismatch return y @override def baz() -> int: # type error: no overridden method return 1 Note: Method decorators @staticmethod and @classmethod should always be applied last - in other words, @override should wrap the method more closely (should only matter for runtime). from typing import override class Base: @staticmethod def foo() -> int: return 1 @classmethod def bar(cls, x: str) -> str: return x class Child(Base): @staticmethod @override def foo() -> int: return 2 @override def bar(self, x: str) -> str: # type error: signature mismatch return x Adoption: The proposed syntax and semantics above imply that the use of the @override decorator will be purely opt-in. Alternatively, type checkers may want to offer an option for users to flag methods that are actively overriding a method in the parent class but are missing the decorator. There are a couple ways this may be possible: 1. Class Inheritance: Denote a class as “enforcing explicit overrides” (syntax suggestions welcome) and error on any subclass methods that override parent methods but are not decorated with @override. This is what the PyPi runtime library (see below) provides via inheritance of a helper EnforceOverrides class. It would affect downstream dependencies that may subclass your logic but not be type checked together, and may be confusing 1. Everywhere: Make explicit annotation of @override a core expectation of well-annotated code, enforcing its usage in strictly typed modules across the board. This is probably too invasive. Precedent in other languages: A non-exhaustive list of other languages that provide the same feature: * Hack has <<__Override>><https://docs.hhvm.com/hack/attributes/predefined-attributes#__override> * Java has @Override<https://docs.oracle.com/javase/tutorial/java/IandI/override.html> * Scala has override<https://www.javatpoint.com/scala-method-overriding> * Kotlin has override<https://kotlinlang.org/docs/inheritance.html#overriding-methods> * Swift has override<https://docs.swift.org/swift-book/LanguageGuide/Inheritance.html#ID198> * C++ has override<https://en.cppreference.com/w/cpp/language/override> * JS/Flow discourage classes (in favor of functions) Comparison to third-party runtime enforcement library: There already exists a library<https://pypi.org/project/overrides/> that enforces @overrides (sic) and @final at runtime. PEP 591<https://peps.python.org/pep-0591/> added a final decorator to the typing module. The override component of the runtime library is not supported statically at all, which has added some confusion around the mix/matched support where the runtime library is imported and used. The ways in which the current static typing.override proposal differs from the runtime library are: 1. Proposed spelling is override rather than overrides 2. May not enforce an option where methods in classes inheriting from EnforceOverrides that are missing this decorator will throw type errors* *The runtime library provides two motivation examples for EnforceOverrides unrelated to feature adoption that I do not find motivating for the type checking use case: 1. “A method that is added to a superclass is shadowed by an existing method with the same name in a subclass.” The type checker will throw errors already here, unless the name, all parameters types and names (and arity), and return type are all perfectly compatible. That seems extremely unlikely unless the methods are in fact intended to do the same thing, which suggests this is not flagging a real error. 1. “A method of a superclass that is overridden by a subclass but the signature of the overridden method is incompatible with that of the inherited one.” Similarly, the type checker will already throw incompatible override errors to flag this. Looking for feedback on whether this would make a good addition to the typing module, as well as thoughts on some of the open questions regarding an “EnforceOverrides” equivalent or any other concerns. Thanks! Shannon
pyright currently supports this in configuration as a global flag that effectively makes override apply to all classes. reportIncompatibleMethodOverride is setting and I have it on for 100k line codebase and have rarely seen cases where I disagree with rule. I can always use type: ignore if needed. I prefer that solution over an override decorator. Most classes in my codebase I consider overriding and changing supported signature an error. I'd rather make incompatible override case require comment/explanation than mark cases where override rule is enforced. Mypy also automatically detects incompatible overrides. I can't even see a flag for it so looks like a default rule mypy applies. Here's an example with mypy enforcing rule by default, https://mypy-play.net/?mypy=latest&python=3.10&flags=strict&gist=d3e1a06857e4c6f1c698b7fa89e22f40.
I suspect that from the users' perspective @override is too similar to @overload and thus easy to mistype or just use by accident (and get surprising type errors). Java, Kotlin etc do not require an annotation for overloading, so they don't have this problem. On Fri, May 20, 2022 at 8:54 PM Mehdi2277 <med2277@gmail.com> wrote:
pyright currently supports this in configuration as a global flag that effectively makes override apply to all classes. reportIncompatibleMethodOverride is setting and I have it on for 100k line codebase and have rarely seen cases where I disagree with rule. I can always use type: ignore if needed. I prefer that solution over an override decorator. Most classes in my codebase I consider overriding and changing supported signature an error. I'd rather make incompatible override case require comment/explanation than mark cases where override rule is enforced.
Mypy also automatically detects incompatible overrides. I can't even see a flag for it so looks like a default rule mypy applies. Here's an example with mypy enforcing rule by default, https://mypy-play.net/?mypy=latest&python=3.10&flags=strict&gist=d3e1a06857e4c6f1c698b7fa89e22f40 . _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: sergei.a.lebedev@gmail.com
We have just added this feature to pytype as well (checking base class signature compatibility whenever overriding any method), based on user requests. martin On Fri, May 20, 2022 at 12:54 PM Mehdi2277 <med2277@gmail.com> wrote:
pyright currently supports this in configuration as a global flag that effectively makes override apply to all classes. reportIncompatibleMethodOverride is setting and I have it on for 100k line codebase and have rarely seen cases where I disagree with rule. I can always use type: ignore if needed. I prefer that solution over an override decorator. Most classes in my codebase I consider overriding and changing supported signature an error. I'd rather make incompatible override case require comment/explanation than mark cases where override rule is enforced.
Mypy also automatically detects incompatible overrides. I can't even see a flag for it so looks like a default rule mypy applies. Here's an example with mypy enforcing rule by default, https://mypy-play.net/?mypy=latest&python=3.10&flags=strict&gist=d3e1a06857e4c6f1c698b7fa89e22f40 . _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: mdemello@google.com
I prefer that solution over an override decorator.
If I read the proposal correctly, the point of the `@override` decorator is not to enforce incompatible method override check. I believe most type checkers already enforce that, as you pointed out. Adding another decorator for that purpose would be obviously redundant. Instead, the purpose of the decorator is to catch cases where the user intends to do an override, but actually did not. For example, ``` class Base: def foo_renamed(self) -> int: ... class Derived(Base): def foo(self) -> int: ... ``` The above code snippet does not contain any incompatible method override issue. It is possible that `Base.foo_renamed()` and `Derived.foo()` are just two completely unrelated methods. But there's also the possibility that `Derived.foo()` does intend to override something in the base class -- perhaps that method was called `Base.foo()` originally but the developer has renamed `Base.foo()` to `Base.foo_renamed()` at some point yet forgets to update the corresponding method name in `Derived`. The proposed `@overload` decorator provides a way to differentiate between these two scenarios, so the type checker can emit a warning when `Derived.foo()` is actually introducing a new method name instead of overriding any pre-existing methods. - Jia
I think this is a useful construct, and I like the proposal. TypeScript has an `overload` keyword for this purpose (see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-3.ht...). A decorator is a natural way to add this functionality in Python. I agree that this should be opt-in at the method level and should not be pushed as a core expectation for all well-annotated code. -- Eric Traut Contributor to Pyright & Pylance Microsoft
Right, just to clarify – the proposal is not to start erroring on incompatible overrides (which I think all type checkers do already), but to support a way to mark a method as “this method must override something in the parent – it is a type error if that is not true”. Some questions: * Naming confusions with “overload” does seem to be an issue. I’m not sure if there is a good alternative name besides “override”, though. Thoughts? * Should we support marking a class as “enforcing explicit overrides”, which means any subclasses must use the “override” decorator to override any methods in the parent? I agree with Eric that we shouldn’t push this as a core expectation, but curious if also having a class level opt-in allows for more confidence when refactoring than only a per-method opt-in. From: Eric Traut <eric@traut.com> Date: Friday, May 20, 2022 at 7:38 PM To: typing-sig@python.org <typing-sig@python.org> Subject: [Typing-sig] Re: typing.override decorator I think this is a useful construct, and I like the proposal. TypeScript has an `overload` keyword for this purpose (see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-3.html#override-and-the---noimplicitoverride-flag<https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-3.html#override-and-the---noimplicitoverride-flag> ). A decorator is a natural way to add this functionality in Python. I agree that this should be opt-in at the method level and should not be pushed as a core expectation for all well-annotated code. -- Eric Traut Contributor to Pyright & Pylance Microsoft _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/<https://mail.python.org/mailman3/lists/typing-sig.python.org/> Member address: szhu@fb.com
We’ve seen runtime bug reports usually caused by an engineer renaming a parent class method and forgetting to rename that method in all child classes.
This is an interesting case -- the new pytype feature Martin mentioned wouldn't catch this. If it wasn't for all the other languages, I'd lean towards this being an IDE feature that's powered by the type checkers. -- Teddy On Mon, May 23, 2022 at 12:49 PM Shannon Zhu via Typing-sig < typing-sig@python.org> wrote:
Right, just to clarify – the proposal is not to start erroring on incompatible overrides (which I think all type checkers do already), but to support a way to mark a method as “this method must override something in the parent – it is a type error if that is not true”.
Some questions:
- Naming confusions with “overload” does seem to be an issue. I’m not sure if there is a good alternative name besides “override”, though. Thoughts? - Should we support marking a class as “enforcing explicit overrides”, which means any subclasses must use the “override” decorator to override any methods in the parent? I agree with Eric that we shouldn’t push this as a core expectation, but curious if also having a class level opt-in allows for more confidence when refactoring than only a per-method opt-in.
*From: *Eric Traut <eric@traut.com> *Date: *Friday, May 20, 2022 at 7:38 PM *To: *typing-sig@python.org <typing-sig@python.org> *Subject: *[Typing-sig] Re: typing.override decorator
I think this is a useful construct, and I like the proposal.
TypeScript has an `overload` keyword for this purpose (see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-3.ht... ). A decorator is a natural way to add this functionality in Python.
I agree that this should be opt-in at the method level and should not be pushed as a core expectation for all well-annotated code.
-- Eric Traut Contributor to Pyright & Pylance Microsoft _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: szhu@fb.com _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: tsudol@google.com
Hi all, Quite coincidentally I recently asked about this on stackoverflow ( https://stackoverflow.com/questions/72316756/python-mark-method-as-implement...). The purpose was twofold, first and foremost tell my fellow programmer that I intend to override/implement a base class/protocol method. Scond, allow for tools to detect that a parent method would be renamed and the child method would not actually override anything. Regarding the potential naming confusion: I am convinced this is a temporary thing. Yes, there will be mistakes, but no more than mistaking ValueError and KeyError, or iterator and iterable. This is standard computer science terminology, so there are plenty of reasons to follow it. Python users are smart enough to pick up the difference ;). Kind regards Kristoffel On Mon, May 23, 2022 at 10:24 PM Teddy Sudol via Typing-sig < typing-sig@python.org> wrote:
We’ve seen runtime bug reports usually caused by an engineer renaming a parent class method and forgetting to rename that method in all child classes.
This is an interesting case -- the new pytype feature Martin mentioned wouldn't catch this. If it wasn't for all the other languages, I'd lean towards this being an IDE feature that's powered by the type checkers.
-- Teddy
On Mon, May 23, 2022 at 12:49 PM Shannon Zhu via Typing-sig < typing-sig@python.org> wrote:
Right, just to clarify – the proposal is not to start erroring on incompatible overrides (which I think all type checkers do already), but to support a way to mark a method as “this method must override something in the parent – it is a type error if that is not true”.
Some questions:
- Naming confusions with “overload” does seem to be an issue. I’m not sure if there is a good alternative name besides “override”, though. Thoughts? - Should we support marking a class as “enforcing explicit overrides”, which means any subclasses must use the “override” decorator to override any methods in the parent? I agree with Eric that we shouldn’t push this as a core expectation, but curious if also having a class level opt-in allows for more confidence when refactoring than only a per-method opt-in.
*From: *Eric Traut <eric@traut.com> *Date: *Friday, May 20, 2022 at 7:38 PM *To: *typing-sig@python.org <typing-sig@python.org> *Subject: *[Typing-sig] Re: typing.override decorator
I think this is a useful construct, and I like the proposal.
TypeScript has an `overload` keyword for this purpose (see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-3.ht... ). A decorator is a natural way to add this functionality in Python.
I agree that this should be opt-in at the method level and should not be pushed as a core expectation for all well-annotated code.
-- Eric Traut Contributor to Pyright & Pylance Microsoft _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: szhu@fb.com _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: tsudol@google.com
_______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: kristoffel.pirard@gmail.com
participants (8)
-
Eric Traut
-
Jia Chen
-
Kristoffel Pirard
-
Martin DeMello
-
Mehdi2277
-
Sergei Lebedev
-
Shannon Zhu
-
Teddy Sudol