From 792ec3e96ed83e182d53f3e5311f62db2dddf48b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 27 Nov 2025 08:18:21 +0000 Subject: [PATCH] Improve docs on how to stop Ruff and ty disagreeing with each other (#21644) ## Summary Lots of Ruff rules encourage you to make changes that might then cause ty to start complaining about Liskov violations. Most of these Ruff rules already refrain from complaining about a method if they see that the method is decorated with `@override`, but this usually isn't documented. This PR updates the docs of many Ruff rules to note that they refrain from complaining about `@override`-decorated methods, and also adds a similar note to the ty `invalid-method-override` documentation. Helps with https://github.com/astral-sh/ty/issues/1644#issuecomment-3581663859 ## Test Plan - `uvx prek run -a` locally - CI on this PR --- ...olean_default_value_positional_argument.rs | 7 ++++ .../boolean_type_hint_positional_argument.rs | 4 ++- .../rules/builtin_argument_shadowing.rs | 5 +++ .../rules/unused_arguments.rs | 36 +++++++++++++++++++ .../rules/invalid_argument_name.rs | 4 ++- .../rules/invalid_function_name.rs | 6 ++++ .../src/rules/pydocstyle/rules/not_missing.rs | 9 +++++ .../pylint/rules/bad_dunder_method_name.rs | 4 ++- .../src/rules/pylint/rules/no_self_use.rs | 12 +++++++ .../rules/pylint/rules/too_many_arguments.rs | 12 +++++++ .../rules/too_many_positional_arguments.rs | 12 +++++++ crates/ty/docs/rules.md | 21 +++++++++++ .../src/types/diagnostic.rs | 20 +++++++++++ ty.schema.json | 2 +- 14 files changed, 150 insertions(+), 4 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_default_value_positional_argument.rs b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_default_value_positional_argument.rs index 361f1df069..cfe817a259 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_default_value_positional_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_default_value_positional_argument.rs @@ -25,6 +25,11 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def; /// keyword-only argument, to force callers to be explicit when providing /// the argument. /// +/// This rule exempts methods decorated with [`@typing.override`][override], +/// since changing the signature of a subclass method that overrides a +/// superclass method may cause type checkers to complain about a violation of +/// the Liskov Substitution Principle. +/// /// ## Example /// ```python /// from math import ceil, floor @@ -89,6 +94,8 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def; /// ## References /// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) /// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/) +/// +/// [override]: https://docs.python.org/3/library/typing.html#typing.override #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.127")] pub(crate) struct BooleanDefaultValuePositionalArgument; diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs index 4bc72d0f26..05a87a28c4 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs @@ -28,7 +28,7 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def; /// the argument. /// /// Dunder methods that define operators are exempt from this rule, as are -/// setters and `@override` definitions. +/// setters and [`@override`][override] definitions. /// /// ## Example /// @@ -93,6 +93,8 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def; /// ## References /// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) /// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/) +/// +/// [override]: https://docs.python.org/3/library/typing.html#typing.override #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.127")] pub(crate) struct BooleanTypeHintPositionalArgument; diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs index 766e4b4561..2545d9e8fd 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs @@ -17,6 +17,8 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin; /// non-obvious errors, as readers may mistake the argument for the /// builtin and vice versa. /// +/// Function definitions decorated with [`@override`][override] or +/// [`@overload`][overload] are exempt from this rule by default. /// Builtins can be marked as exceptions to this rule via the /// [`lint.flake8-builtins.ignorelist`] configuration option. /// @@ -48,6 +50,9 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin; /// ## References /// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide) /// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python) +/// +/// [override]: https://docs.python.org/3/library/typing.html#typing.override +/// [overload]: https://docs.python.org/3/library/typing.html#typing.overload #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.48")] pub(crate) struct BuiltinArgumentShadowing { diff --git a/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs b/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs index 18d0a96377..884bb00a3e 100644 --- a/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs +++ b/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs @@ -60,6 +60,16 @@ impl Violation for UnusedFunctionArgument { /// prefixed with an underscore, or some other value that adheres to the /// [`lint.dummy-variable-rgx`] pattern. /// +/// This rule exempts methods decorated with [`@typing.override`][override]. +/// Removing a parameter from a subclass method (or changing a parameter's +/// name) may cause type checkers to complain about a violation of the Liskov +/// Substitution Principle if it means that the method now incompatibly +/// overrides a method defined on a superclass. Explicitly decorating an +/// overriding method with `@override` signals to Ruff that the method is +/// intended to override a superclass method and that a type checker will +/// enforce that it does so; Ruff therefore knows that it should not enforce +/// rules about unused arguments on such methods. +/// /// ## Example /// ```python /// class Class: @@ -76,6 +86,8 @@ impl Violation for UnusedFunctionArgument { /// /// ## Options /// - `lint.dummy-variable-rgx` +/// +/// [override]: https://docs.python.org/3/library/typing.html#typing.override #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.168")] pub(crate) struct UnusedMethodArgument { @@ -101,6 +113,16 @@ impl Violation for UnusedMethodArgument { /// prefixed with an underscore, or some other value that adheres to the /// [`lint.dummy-variable-rgx`] pattern. /// +/// This rule exempts methods decorated with [`@typing.override`][override]. +/// Removing a parameter from a subclass method (or changing a parameter's +/// name) may cause type checkers to complain about a violation of the Liskov +/// Substitution Principle if it means that the method now incompatibly +/// overrides a method defined on a superclass. Explicitly decorating an +/// overriding method with `@override` signals to Ruff that the method is +/// intended to override a superclass method and that a type checker will +/// enforce that it does so; Ruff therefore knows that it should not enforce +/// rules about unused arguments on such methods. +/// /// ## Example /// ```python /// class Class: @@ -119,6 +141,8 @@ impl Violation for UnusedMethodArgument { /// /// ## Options /// - `lint.dummy-variable-rgx` +/// +/// [override]: https://docs.python.org/3/library/typing.html#typing.override #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.168")] pub(crate) struct UnusedClassMethodArgument { @@ -144,6 +168,16 @@ impl Violation for UnusedClassMethodArgument { /// prefixed with an underscore, or some other value that adheres to the /// [`lint.dummy-variable-rgx`] pattern. /// +/// This rule exempts methods decorated with [`@typing.override`][override]. +/// Removing a parameter from a subclass method (or changing a parameter's +/// name) may cause type checkers to complain about a violation of the Liskov +/// Substitution Principle if it means that the method now incompatibly +/// overrides a method defined on a superclass. Explicitly decorating an +/// overriding method with `@override` signals to Ruff that the method is +/// intended to override a superclass method, and that a type checker will +/// enforce that it does so; Ruff therefore knows that it should not enforce +/// rules about unused arguments on such methods. +/// /// ## Example /// ```python /// class Class: @@ -162,6 +196,8 @@ impl Violation for UnusedClassMethodArgument { /// /// ## Options /// - `lint.dummy-variable-rgx` +/// +/// [override]: https://docs.python.org/3/library/typing.html#typing.override #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.168")] pub(crate) struct UnusedStaticMethodArgument { diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_argument_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_argument_name.rs index a490f34702..b3ee458b31 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_argument_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_argument_name.rs @@ -23,7 +23,7 @@ use crate::checkers::ast::Checker; /// > mixedCase is allowed only in contexts where that’s already the /// > prevailing style (e.g. threading.py), to retain backwards compatibility. /// -/// Methods decorated with `@typing.override` are ignored. +/// Methods decorated with [`@typing.override`][override] are ignored. /// /// ## Example /// ```python @@ -43,6 +43,8 @@ use crate::checkers::ast::Checker; /// /// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments /// [preview]: https://docs.astral.sh/ruff/preview/ +/// +/// [override]: https://docs.python.org/3/library/typing.html#typing.override #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.77")] pub(crate) struct InvalidArgumentName { diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_function_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_function_name.rs index 546b41c014..d7fbd985dd 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_function_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_function_name.rs @@ -24,6 +24,11 @@ use crate::rules::pep8_naming::settings::IgnoreNames; /// to ignore all functions starting with `test_` from this rule, set the /// [`lint.pep8-naming.extend-ignore-names`] option to `["test_*"]`. /// +/// This rule exempts methods decorated with [`@typing.override`][override]. +/// Explicitly decorating a method with `@override` signals to Ruff that the method is intended +/// to override a superclass method, and that a type checker will enforce that it does so. Ruff +/// therefore knows that it should not enforce naming conventions on such methods. +/// /// ## Example /// ```python /// def myFunction(): @@ -41,6 +46,7 @@ use crate::rules::pep8_naming::settings::IgnoreNames; /// - `lint.pep8-naming.extend-ignore-names` /// /// [PEP 8]: https://peps.python.org/pep-0008/#function-and-variable-names +/// [override]: https://docs.python.org/3/library/typing.html#typing.override #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.77")] pub(crate) struct InvalidFunctionName { diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/not_missing.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/not_missing.rs index b82098169f..24ed1e4856 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/not_missing.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/not_missing.rs @@ -169,7 +169,12 @@ impl Violation for UndocumentedPublicClass { /// If the codebase adheres to a standard format for method docstrings, follow /// that format for consistency. /// +/// This rule exempts methods decorated with [`@typing.override`][override], +/// since it is a common practice to document a method on a superclass but not +/// on an overriding method in a subclass. +/// /// ## Example +/// /// ```python /// class Cat(Animal): /// def greet(self, happy: bool = True): @@ -180,6 +185,7 @@ impl Violation for UndocumentedPublicClass { /// ``` /// /// Use instead (in the NumPy docstring format): +/// /// ```python /// class Cat(Animal): /// def greet(self, happy: bool = True): @@ -202,6 +208,7 @@ impl Violation for UndocumentedPublicClass { /// ``` /// /// Or (in the Google docstring format): +/// /// ```python /// class Cat(Animal): /// def greet(self, happy: bool = True): @@ -227,6 +234,8 @@ impl Violation for UndocumentedPublicClass { /// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) +/// +/// [override]: https://docs.python.org/3/library/typing.html#typing.override #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.70")] pub(crate) struct UndocumentedPublicMethod; diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_dunder_method_name.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_dunder_method_name.rs index 200ec4dbe2..a375147665 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_dunder_method_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_dunder_method_name.rs @@ -21,7 +21,7 @@ use crate::rules::pylint::helpers::is_known_dunder_method; /// /// This rule will detect all methods starting and ending with at least /// one underscore (e.g., `_str_`), but ignores known dunder methods (like -/// `__init__`), as well as methods that are marked with `@override`. +/// `__init__`), as well as methods that are marked with [`@override`][override]. /// /// Additional dunder methods names can be allowed via the /// [`lint.pylint.allow-dunder-method-names`] setting. @@ -42,6 +42,8 @@ use crate::rules::pylint::helpers::is_known_dunder_method; /// /// ## Options /// - `lint.pylint.allow-dunder-method-names` +/// +/// [override]: https://docs.python.org/3/library/typing.html#typing.override #[derive(ViolationMetadata)] #[violation_metadata(preview_since = "v0.0.285")] pub(crate) struct BadDunderMethodName { diff --git a/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs b/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs index fd6e24528c..c6a2fe9d0d 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs @@ -17,6 +17,16 @@ use crate::rules::flake8_unused_arguments::rules::is_not_implemented_stub_with_v /// Unused `self` parameters are usually a sign of a method that could be /// replaced by a function, class method, or static method. /// +/// This rule exempts methods decorated with [`@typing.override`][override]. +/// Converting an instance method into a static method or class method may +/// cause type checkers to complain about a violation of the Liskov +/// Substitution Principle if it means that the method now incompatibly +/// overrides a method defined on a superclass. Explicitly decorating an +/// overriding method with `@override` signals to Ruff that the method is +/// intended to override a superclass method and that a type checker will +/// enforce that it does so; Ruff therefore knows that it should not enforce +/// rules about unused `self` parameters on such methods. +/// /// ## Example /// ```python /// class Person: @@ -38,6 +48,8 @@ use crate::rules::flake8_unused_arguments::rules::is_not_implemented_stub_with_v /// def greeting(): /// print("Greetings friend!") /// ``` +/// +/// [override]: https://docs.python.org/3/library/typing.html#typing.override #[derive(ViolationMetadata)] #[violation_metadata(preview_since = "v0.0.286")] pub(crate) struct NoSelfUse { diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_arguments.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_arguments.rs index 9242ad8342..0fb6b4f04e 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_arguments.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_arguments.rs @@ -12,6 +12,16 @@ use crate::checkers::ast::Checker; /// By default, this rule allows up to five arguments, as configured by the /// [`lint.pylint.max-args`] option. /// +/// This rule exempts methods decorated with [`@typing.override`][override]. +/// Changing the signature of a subclass method may cause type checkers to +/// complain about a violation of the Liskov Substitution Principle if it +/// means that the method now incompatibly overrides a method defined on a +/// superclass. Explicitly decorating an overriding method with `@override` +/// signals to Ruff that the method is intended to override a superclass +/// method and that a type checker will enforce that it does so; Ruff +/// therefore knows that it should not enforce rules about methods having +/// too many arguments. +/// /// ## Why is this bad? /// Functions with many arguments are harder to understand, maintain, and call. /// Consider refactoring functions with many arguments into smaller functions @@ -43,6 +53,8 @@ use crate::checkers::ast::Checker; /// /// ## Options /// - `lint.pylint.max-args` +/// +/// [override]: https://docs.python.org/3/library/typing.html#typing.override #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.238")] pub(crate) struct TooManyArguments { diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs index f9f979658c..db661c68b7 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs @@ -21,6 +21,16 @@ use crate::checkers::ast::Checker; /// with fewer arguments, using objects to group related arguments, or migrating to /// [keyword-only arguments](https://docs.python.org/3/tutorial/controlflow.html#special-parameters). /// +/// This rule exempts methods decorated with [`@typing.override`][override]. +/// Changing the signature of a subclass method may cause type checkers to +/// complain about a violation of the Liskov Substitution Principle if it +/// means that the method now incompatibly overrides a method defined on a +/// superclass. Explicitly decorating an overriding method with `@override` +/// signals to Ruff that the method is intended to override a superclass +/// method and that a type checker will enforce that it does so; Ruff +/// therefore knows that it should not enforce rules about methods having +/// too many arguments. +/// /// ## Example /// /// ```python @@ -41,6 +51,8 @@ use crate::checkers::ast::Checker; /// /// ## Options /// - `lint.pylint.max-positional-args` +/// +/// [override]: https://docs.python.org/3/library/typing.html#typing.override #[derive(ViolationMetadata)] #[violation_metadata(preview_since = "v0.1.7")] pub(crate) struct TooManyPositionalArguments { diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 730692f9ef..3a77ed7946 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -1012,7 +1012,28 @@ classes in Python do indeed behave this way, the strongly held convention is tha be avoided wherever possible. As part of this check, therefore, ty enforces that `__eq__` and `__ne__` methods accept `object` as their second argument. +**Why does ty disagree with Ruff about how to write my method?** + + +Ruff has several rules that will encourage you to rename a parameter, or change its type +signature, if it thinks you're falling into a certain anti-pattern. For example, Ruff's +[ARG002](https://docs.astral.sh/ruff/rules/unused-method-argument/) rule recommends that an +unused parameter should either be removed or renamed to start with `_`. Applying either of +these suggestions can cause ty to start reporting an `invalid-method-override` error if +the function in question is a method on a subclass that overrides a method on a superclass, +and the change would cause the subclass method to no longer accept all argument combinations +that the superclass method accepts. + +This can usually be resolved by adding [`@typing.override`][override] to your method +definition. Ruff knows that a method decorated with `@typing.override` is intended to +override a method by the same name on a superclass, and avoids reporting rules like ARG002 +for such methods; it knows that the changes recommended by ARG002 would violate the Liskov +Substitution Principle. + +Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. + [Liskov Substitution Principle]: https://en.wikipedia.org/wiki/Liskov_substitution_principle +[override]: https://docs.python.org/3/library/typing.html#typing.override ## `invalid-named-tuple` diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index a3490337b0..44b54d0c41 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -2114,7 +2114,27 @@ declare_lint! { /// be avoided wherever possible. As part of this check, therefore, ty enforces that `__eq__` /// and `__ne__` methods accept `object` as their second argument. /// + /// ### Why does ty disagree with Ruff about how to write my method? + /// + /// Ruff has several rules that will encourage you to rename a parameter, or change its type + /// signature, if it thinks you're falling into a certain anti-pattern. For example, Ruff's + /// [ARG002](https://docs.astral.sh/ruff/rules/unused-method-argument/) rule recommends that an + /// unused parameter should either be removed or renamed to start with `_`. Applying either of + /// these suggestions can cause ty to start reporting an `invalid-method-override` error if + /// the function in question is a method on a subclass that overrides a method on a superclass, + /// and the change would cause the subclass method to no longer accept all argument combinations + /// that the superclass method accepts. + /// + /// This can usually be resolved by adding [`@typing.override`][override] to your method + /// definition. Ruff knows that a method decorated with `@typing.override` is intended to + /// override a method by the same name on a superclass, and avoids reporting rules like ARG002 + /// for such methods; it knows that the changes recommended by ARG002 would violate the Liskov + /// Substitution Principle. + /// + /// Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. + /// /// [Liskov Substitution Principle]: https://en.wikipedia.org/wiki/Liskov_substitution_principle + /// [override]: https://docs.python.org/3/library/typing.html#typing.override pub(crate) static INVALID_METHOD_OVERRIDE = { summary: "detects method definitions that violate the Liskov Substitution Principle", status: LintStatus::stable("0.0.1-alpha.20"), diff --git a/ty.schema.json b/ty.schema.json index aef426eb23..919b0e8dfc 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -635,7 +635,7 @@ }, "invalid-method-override": { "title": "detects method definitions that violate the Liskov Substitution Principle", - "description": "## What it does\nDetects method overrides that violate the [Liskov Substitution Principle] (\"LSP\").\n\nThe LSP states that an instance of a subtype should be substitutable for an instance of its supertype.\nApplied to Python, this means:\n1. All argument combinations a superclass method accepts\n must also be accepted by an overriding subclass method.\n2. The return type of an overriding subclass method must be a subtype\n of the return type of the superclass method.\n\n## Why is this bad?\nViolating the Liskov Substitution Principle will lead to many of ty's assumptions and\ninferences being incorrect, which will mean that it will fail to catch many possible\ntype errors in your code.\n\n## Example\n```python\nclass Super:\n def method(self, x) -> int:\n return 42\n\nclass Sub(Super):\n # Liskov violation: `str` is not a subtype of `int`,\n # but the supertype method promises to return an `int`.\n def method(self, x) -> str: # error: [invalid-override]\n return \"foo\"\n\ndef accepts_super(s: Super) -> int:\n return s.method(x=42)\n\naccepts_super(Sub()) # The result of this call is a string, but ty will infer\n # it to be an `int` due to the violation of the Liskov Substitution Principle.\n\nclass Sub2(Super):\n # Liskov violation: the superclass method can be called with a `x=`\n # keyword argument, but the subclass method does not accept it.\n def method(self, y) -> int: # error: [invalid-override]\n return 42\n\naccepts_super(Sub2()) # TypeError at runtime: method() got an unexpected keyword argument 'x'\n # ty cannot catch this error due to the violation of the Liskov Substitution Principle.\n```\n\n## Common issues\n\n### Why does ty complain about my `__eq__` method?\n\n`__eq__` and `__ne__` methods in Python are generally expected to accept arbitrary\nobjects as their second argument, for example:\n\n```python\nclass A:\n x: int\n\n def __eq__(self, other: object) -> bool:\n # gracefully handle an object of an unexpected type\n # without raising an exception\n if not isinstance(other, A):\n return False\n return self.x == other.x\n```\n\nIf `A.__eq__` here were annotated as only accepting `A` instances for its second argument,\nit would imply that you wouldn't be able to use `==` between instances of `A` and\ninstances of unrelated classes without an exception possibly being raised. While some\nclasses in Python do indeed behave this way, the strongly held convention is that it should\nbe avoided wherever possible. As part of this check, therefore, ty enforces that `__eq__`\nand `__ne__` methods accept `object` as their second argument.\n\n[Liskov Substitution Principle]: https://en.wikipedia.org/wiki/Liskov_substitution_principle", + "description": "## What it does\nDetects method overrides that violate the [Liskov Substitution Principle] (\"LSP\").\n\nThe LSP states that an instance of a subtype should be substitutable for an instance of its supertype.\nApplied to Python, this means:\n1. All argument combinations a superclass method accepts\n must also be accepted by an overriding subclass method.\n2. The return type of an overriding subclass method must be a subtype\n of the return type of the superclass method.\n\n## Why is this bad?\nViolating the Liskov Substitution Principle will lead to many of ty's assumptions and\ninferences being incorrect, which will mean that it will fail to catch many possible\ntype errors in your code.\n\n## Example\n```python\nclass Super:\n def method(self, x) -> int:\n return 42\n\nclass Sub(Super):\n # Liskov violation: `str` is not a subtype of `int`,\n # but the supertype method promises to return an `int`.\n def method(self, x) -> str: # error: [invalid-override]\n return \"foo\"\n\ndef accepts_super(s: Super) -> int:\n return s.method(x=42)\n\naccepts_super(Sub()) # The result of this call is a string, but ty will infer\n # it to be an `int` due to the violation of the Liskov Substitution Principle.\n\nclass Sub2(Super):\n # Liskov violation: the superclass method can be called with a `x=`\n # keyword argument, but the subclass method does not accept it.\n def method(self, y) -> int: # error: [invalid-override]\n return 42\n\naccepts_super(Sub2()) # TypeError at runtime: method() got an unexpected keyword argument 'x'\n # ty cannot catch this error due to the violation of the Liskov Substitution Principle.\n```\n\n## Common issues\n\n### Why does ty complain about my `__eq__` method?\n\n`__eq__` and `__ne__` methods in Python are generally expected to accept arbitrary\nobjects as their second argument, for example:\n\n```python\nclass A:\n x: int\n\n def __eq__(self, other: object) -> bool:\n # gracefully handle an object of an unexpected type\n # without raising an exception\n if not isinstance(other, A):\n return False\n return self.x == other.x\n```\n\nIf `A.__eq__` here were annotated as only accepting `A` instances for its second argument,\nit would imply that you wouldn't be able to use `==` between instances of `A` and\ninstances of unrelated classes without an exception possibly being raised. While some\nclasses in Python do indeed behave this way, the strongly held convention is that it should\nbe avoided wherever possible. As part of this check, therefore, ty enforces that `__eq__`\nand `__ne__` methods accept `object` as their second argument.\n\n### Why does ty disagree with Ruff about how to write my method?\n\nRuff has several rules that will encourage you to rename a parameter, or change its type\nsignature, if it thinks you're falling into a certain anti-pattern. For example, Ruff's\n[ARG002](https://docs.astral.sh/ruff/rules/unused-method-argument/) rule recommends that an\nunused parameter should either be removed or renamed to start with `_`. Applying either of\nthese suggestions can cause ty to start reporting an `invalid-method-override` error if\nthe function in question is a method on a subclass that overrides a method on a superclass,\nand the change would cause the subclass method to no longer accept all argument combinations\nthat the superclass method accepts.\n\nThis can usually be resolved by adding [`@typing.override`][override] to your method\ndefinition. Ruff knows that a method decorated with `@typing.override` is intended to\noverride a method by the same name on a superclass, and avoids reporting rules like ARG002\nfor such methods; it knows that the changes recommended by ARG002 would violate the Liskov\nSubstitution Principle.\n\nCorrect use of `@override` is enforced by ty's `invalid-explicit-override` rule.\n\n[Liskov Substitution Principle]: https://en.wikipedia.org/wiki/Liskov_substitution_principle\n[override]: https://docs.python.org/3/library/typing.html#typing.override", "default": "error", "oneOf": [ {