Skip to content

Improve stubtest handling of expected dunder methods#21764

Open
AlexWaygood wants to merge 2 commits into
python:masterfrom
AlexWaygood:alex/stubtest-expected-dunder-wrappers
Open

Improve stubtest handling of expected dunder methods#21764
AlexWaygood wants to merge 2 commits into
python:masterfrom
AlexWaygood:alex/stubtest-expected-dunder-wrappers

Conversation

@AlexWaygood

@AlexWaygood AlexWaygood commented Jul 21, 2026

Copy link
Copy Markdown
Member

Stubtest currently suppresses diagnostics regarding missing dunder methods whenever the runtime attribute is a WrapperDescriptorType. This is because WrapperDescriptorType dunders are often not actually callable at runtime, so suppressing these diagnostics avoids many false positives. However, it also hides genuine omissions such as collections.deque.__rmul__ (typeshed#16055).

This PR keeps the existing suppression in the general case while reporting missing methods that the stub gives us a reason to expect:

  • If a stub class has a __add__ method, a diagnostic regarding a missing __radd__ method is probably not a false positive
  • If a a stub class has a __lt__ method, a diagnostic regarding a missing __gt__ method is probably not a false positive
  • If a stub class inherits from Mapping, diagnostics regarding missing __or__, __ror__ or __ior__ are probably not false positives
  • If a stub class inherits from Sequence, diagnostics regarding missing __mul__, __rmul__ or __imul__ methods are probably not false positives.

There may be more heuristics we could add similar to these. These were just the ones I/Codex could think of.

@AlexWaygood
AlexWaygood force-pushed the alex/stubtest-expected-dunder-wrappers branch from 60a3d7f to f519d92 Compare July 21, 2026 15:07
@AlexWaygood

AlexWaygood commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

This patch produces 17 new hits on typeshed's stdlib on Python 3.10–3.14, with three additional hits on Python 3.15. None are currently allowlisted.

builtins.bytearray.__rmod__
builtins.bytes.__rmod__
builtins.frozenset.__rand__
builtins.frozenset.__ror__
builtins.frozenset.__rsub__
builtins.frozenset.__rxor__
builtins.set.__rand__
builtins.set.__ror__
builtins.set.__rsub__
builtins.set.__rxor__
builtins.str.__rmod__
datetime.date.__rsub__
datetime.datetime.__rsub__
datetime.timedelta.__rdivmod__
datetime.timedelta.__rfloordiv__
datetime.timedelta.__rmod__
datetime.timedelta.__rtruediv__

Python 3.15 only:

types.FrameLocalsProxyType.__ior__
types.FrameLocalsProxyType.__or__
types.FrameLocalsProxyType.__ror__

These all appear to be true positives that we should probably fix in typeshed.

Codex analysis of the new hits

All 20 are callable wrapper_descriptors. Here are direct runtime examples. For reflected methods, the first argument is the receiver/right-hand operand and the second represents the left-hand operand.

>>> str.__rmod__("world", "hello %s")
'hello world'
>>> bytes.__rmod__(b"world", b"hello %s")
b'hello world'
>>> bytearray.__rmod__(bytearray(b"world"), bytearray(b"hello %s"))
bytearray(b'hello world')

>>> set.__rand__({2, 3}, {1, 2})
{2}
>>> set.__ror__({2, 3}, {1, 2})
{1, 2, 3}
>>> set.__rsub__({2, 3}, {1, 2})
{1}
>>> set.__rxor__({2, 3}, {1, 2})
{1, 3}

>>> frozenset.__rand__(frozenset({2, 3}), frozenset({1, 2}))
frozenset({2})
>>> frozenset.__ror__(frozenset({2, 3}), frozenset({1, 2}))
frozenset({1, 2, 3})
>>> frozenset.__rsub__(frozenset({2, 3}), frozenset({1, 2}))
frozenset({1})
>>> frozenset.__rxor__(frozenset({2, 3}), frozenset({1, 2}))
frozenset({1, 3})

>>> from datetime import date, datetime, timedelta
>>> date.__rsub__(date(2025, 1, 1), date(2025, 1, 4))
datetime.timedelta(days=3)
>>> datetime.__rsub__(datetime(2025, 1, 1), datetime(2025, 1, 4))
datetime.timedelta(days=3)
>>> timedelta.__rdivmod__(timedelta(days=2), timedelta(days=7))
(3, datetime.timedelta(days=1))
>>> timedelta.__rfloordiv__(timedelta(days=2), timedelta(days=7))
3
>>> timedelta.__rmod__(timedelta(days=2), timedelta(days=7))
datetime.timedelta(days=1)
>>> timedelta.__rtruediv__(timedelta(days=2), timedelta(days=7))
3.5

The three Python 3.15-specific hits are also callable:

>>> import sys
>>> import types
>>> def get_frame():
...     value = 1
...     return sys._getframe()
...
>>> frame = get_frame()
>>> locals_proxy = frame.f_locals
>>> types.FrameLocalsProxyType.__or__(locals_proxy, {"other": 2})
{'value': 1, 'other': 2}
>>> types.FrameLocalsProxyType.__ror__(locals_proxy, {"other": 2})
{'other': 2, 'value': 1}
>>> types.FrameLocalsProxyType.__ior__(locals_proxy, {"other": 2})
{'value': 1, 'other': 2}
>>> frame.f_locals
{'value': 1, 'other': 2}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant