facebook / facebook/pyrefly

`explicit-any` is reported for `@override` methods that must match a base-class signature annotated with `Any`

Open
#4,548 0 comments 0 reactions 0 assignees View on GitHub
typechecking
Dominant language
Rust
Stars
7k
Forks
516
PR merge metrics
No merged PRs in 30d

Description

# Describe the Bug

When a method is decorated with `@typing.override`, its signature is dictated by the base class. If the base class annotates a parameter or the return as `Any`, the override has to repeat that `Any` — narrowing it would change the signature, and removing it is not possible. `explicit-any` currently fires on those annotations anyway, so the diagnostic points at code that cannot be changed.

## Repro

`repro.py`:

```python
import json
from typing import Any
from typing import override

class CustomEncoder(json.JSONEncoder):
@override
def default(self, o: Any) -> Any:
return super().default(o)
```

`pyrefly.toml`:

```toml
[errors]
explicit-any = "error"
```

Actual (pyrefly 1.2.0):

```
ERROR Explicit `Any` is not allowed [explicit-any]
--> repro.py:8:26
ERROR Explicit `Any` is not allowed [explicit-any]
--> repro.py:8:34
INFO 2 errors
```

Expected: no errors. Both annotations are fixed by typeshed's `json.JSONEncoder.default`, which is declared as `def default(self, o: Any) -> Any`.

## Why this seems worth special-casing

Ruff reached the same conclusion for its equivalent lint, ANN401. It exempts `@override` methods, implemented in [astral-sh/ruff#4409](https://github.com/astral-sh/ruff/pull/4409) closing [astral-sh/ruff#4408](https://github.com/astral-sh/ruff/issues/4408). The reasoning given there:

> Where that discussion landed was that it is reasonable to suppress `ARG002` for overridden methods, since you cannot control the method signature. I think it follows that `ANN401` should be suppressed as well, since if we cannot control the method signature we also cannot control the types, and it is impossible to narrow or remove `Any` in such cases.

That earlier `ARG002` discussion is [astral-sh/ruff#3952](https://github.com/astral-sh/ruff/issues/3952).

The practical effect is that in a codebase running `preset = "all"`, `explicit-any` is actionable for first-party annotations but produces unsuppressable noise wherever the codebase subclasses a framework. In ours, every `explicit-any` site in `src/` that we could not remove was an `@override` of a Starlette/syrupy method.

This is a different mechanism from [#4088](https://github.com/facebook/pyrefly/issues/4088) (third-party type aliases that expand to `Any`), but the same underlying theme: `explicit-any` firing on `Any` that the first-party code did not choose.

## Workaround

Per-line suppression on each affected parameter and return:

```python
@override
def default(self, o: Any) -> Any: # pyrefly: ignore[explicit-any]
...
```

One suppression covers every annotation on its line, so the cost is bounded, but it has to be repeated at every override of every framework method. For `*args: Any, **kwargs: Any` overrides it is worse than it looks: a formatter (we use `ruff format`) splits the parameters onto separate lines once the suppression comment makes the line long enough, which strands the annotations that no longer share a line with the comment and forces one suppression per parameter.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.