Overloaded `__getattr__` resolves attributes to the overloaded function
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 20.6k
- Forks
- 3.3k
- PR merge metrics
- PR metrics pending
Description
Bug Report
If __getattr__ is overloaded, all attribute accesses resolves as if they were getting __getattr__ itself. This is most obvious if you expect it to return a callable - mypy will complain that the args to the callable don't match the signatures of __getattr__. This does not happen if you call __getattr__ manually, only via regular attribute access.
To Reproduce
class A:
@overload
def __getattr__(self, name: str) -> Callable[..., Any]: ...
@overload
def __getattr__(self, name: int) -> int: ...
def __getattr__(self, name: Any) -> Any:
pass
reveal_type(A().__getattr__)
reveal_type(A().xyz)
reveal_type(A().__getattr__("xyz"))
reveal_type(A().__getattr__(1))
A().xyz(1, 2, 3)
main.py:7: error: Invalid signature "Callable[[A, int], int]" for "__getattr__" [misc]
main.py:11: note: Revealed type is "Overload(def (builtins.str) -> def (*Any, **Any) -> Any, def (builtins.int) -> builtins.int)"
main.py:12: note: Revealed type is "Overload(def (builtins.str) -> def (*Any, **Any) -> Any, def (builtins.int) -> builtins.int)"
main.py:13: note: Revealed type is "def (*Any, **Any) -> Any"
main.py:14: note: Revealed type is "builtins.int"
main.py:16: error: No overload variant of "__getattr__" of "A" matches argument types "int", "int", "int" [call-overload]
main.py:16: note: Possible overload variants:
main.py:16: note: def __getattr__(self, str, /) -> Callable[..., Any]
main.py:16: note: def __getattr__(self, int, /) -> int
The first error is expected - __getattr__ doesn't normally take ints, I'm just using them to make types clear. In practice, subclasses of str, or literals as in #8203 might make more sense, though they currently all error the same way.
This behavior is the same as accessing, but not calling, an overloaded function elsewhere.
class B:
@overload
def overloaded(self, name: str) -> Callable[..., Any]: ...
@overload
def overloaded(self, name: int) -> int: ...
def overloaded(self, name: Any) -> Any:
pass
reveal_type(B().overloaded)
main.py:26: note: Revealed type is "Overload(def (name: builtins.str) -> def (*Any, **Any) -> Any, def (name: builtins.int) -> builtins.int)"
Expected Behavior
The second revealed type, of A().xyz, should worst case resolve to Callable[..., Any] | int - i.e. when overloaded, attribute access should resolve to the union of the overloads' return values, in the same way other overloads with unknown args do.
x: str | int
reveal_type(B().overloaded(x))
main.py:29: note: Revealed type is "Union[def (*Any, **Any) -> Any, builtins.int]"
Additionally, I think it should be possible to deduce that __getattr__ gets called with a string in normal attribute access, meaning the type of the result can be narrowed, down to just Callable[..., Any] in this case.
Your Environment
Python 3.11, Mypy 1.4.1, default settings
https://mypy-play.net/?mypy=1.4.1&python=3.11&gist=0e629e0ec4d8a4a4a69f8ed979bdd04d
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by running the provided Python snippets with mypy and compare the revealed types for overloaded getattr and ordinary overloaded methods. Trace the type-checking path for attribute access and overload resolution. Done means ordinary access returns the appropriate overload result, ideally narrowed for a string attribute name, without the erroneous call-overload diagnostic.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- compilers, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100