Skip to content

Narrow MutableSet in-place operators - #16634

Open
srittau wants to merge 3 commits into
python:mainfrom
srittau:abstract-set
Open

srittau wants to merge 3 commits into
python:mainfrom
srittau:abstract-set

Conversation

@srittau

@srittau srittau commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

Exploratory PR for narrowing the in-place operators of MutableSet.

Rationale: set and frozenset – which probably make up 99% of AbstractSet implementation uses – have a narrower API that what MutableSet currently accepts: The former only accept set and frozenset operands in their operators. For other AbstractSets, Python falls back to the operand's reflected method.

An underlying problem is the dichotomy of these classes as ABCc and pseudo-protocols: As ABCs they accept more types than the protocol would. This PR limits MutableSet's API to what set and frozenset actually support. This leads to soundness problems:

class MySet(Set[int]): ...  # minimal implementation

s = {1, 2}
ret = s.__iand__(MySet([2]))
reveal_type(ret)  # set[int]; Runtime type is 'NotImplementedType'

def merge(target: set[int], other: Set[int]) -> None:
    target |= other  # target is now a MySet
    target.add(42)  # AttributeError: 'MySet' object has no attribute 'add'

Claude Code (Claude Opus 5.5) (which helped me prepare this PR) added extensive test coverage for this, which I believe is warranted in this fundamental case.

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

mypy (https://github.com/python/mypy)
+ mypyc/analysis/dataflow.py:615: error: Result type of - incompatible in assignment  [misc]
+ mypyc/analysis/dataflow.py:618: error: Result type of | incompatible in assignment  [misc]
+ mypyc/analysis/dataflow.py:619: error: Result type of - incompatible in assignment  [misc]
+ mypyc/analysis/dataflow.py:622: error: Result type of | incompatible in assignment  [misc]
+ mypyc/analysis/dataflow.py:685: error: Incompatible types in assignment (expression has type "AbstractSet[T]", target has type "set[T]")  [assignment]
+ mypyc/analysis/dataflow.py:691: error: Incompatible types in assignment (expression has type "AbstractSet[T]", target has type "set[T]")  [assignment]
+ mypy/stubtest.py:1242: error: Unsupported operand types for | ("set[str]" and "set[str]")  [operator]
+ mypy/stubtest.py:1242: note: See https://mypy.rtfd.io/en/stable/_refs.html#code-operator for more info

core (https://github.com/home-assistant/core)
+ homeassistant/helpers/device_registry.py:275: error: Incompatible types in assignment (expression has type "set[str]", variable has type "frozenset[str]")  [assignment]
+ homeassistant/components/logbook/helpers.py:109: error: Unsupported operand types for | ("set[EventType[Any] | str]" and "set[str]")  [operator]

pegen (https://github.com/we-like-parsers/pegen)
+ src/pegen/grammar.py:180: error: Result type of | incompatible in assignment  [misc]
+ src/pegen/grammar.py:215: error: Result type of | incompatible in assignment  [misc]
+ src/pegen/parser_generator.py:255: error: Result type of - incompatible in assignment  [misc]
+ src/pegen/parser_generator.py:283: error: Result type of | incompatible in assignment  [misc]

CPython (peg_generator) (https://github.com/python/cpython)
+ Tools/peg_generator/pegen/grammar_analysis.py:123: error: Result type of - incompatible in assignment  [misc]

sphinx (https://github.com/sphinx-doc/sphinx)
+ sphinx/ext/coverage.py: note: In function "_load_modules":
+ sphinx/ext/coverage.py:101:13: error: Result type of | incompatible in assignment  [misc]
+ sphinx/ext/coverage.py: note: In function "_determine_py_coverage_modules":
+ sphinx/ext/coverage.py:137:13: error: Result type of | incompatible in assignment  [misc]

@srittau

srittau commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

The primer fallout is lower than feared and needs analysis.

homeassistant/helpers/device_registry.py:275 is actually a true positive:

     __optional_keys__: ClassVar[frozenset[str]]

    ...

    cls.__optional_keys__ = (
        frozenset(cls._field_names)
        - cls.__required_keys__
        - _DEPRECATED_DEVICE_INFO_PARAMETERS.keys()
    )

But:

frozenset() - frozenset() - {}.keys()  # -> set()

@srittau

srittau commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

Primer analysis:

  1. mypyc (code link): True positive, although probably not a problem at runtime, at the moment. But could become a problem if accept methods would start to return a non-set/frozenset AbstractSets. Needs update to type annotations.
  2. stubtest (code link): Fix invalid contextual inference for generic call arguments mypy#21803; the mypy PR will fix this primer hit
  3. homeassistant no. 1 (code link): See above, true positive. Class var annotation does not match reality.
  4. homeassistant no. 2 (code link): Also Fix invalid contextual inference for generic call arguments mypy#21803
  5. pegen grammar.py (code link 1) (code link 2): Semi-true positive: names is explicitly set[str], but name |= alt.initial_names() may change that. May be relevant if it's changes to something other than an AbstractSet[str], though.
  6. pegen parser_generator.py no. 1 (code link): Semi-true positive: leaders = set(scc) initializes leaders as a set. Later, leaders -= scc - set(cycle) may change the type. Irrelevant in practice.
  7. pegen parser_generator.py no. 2 (code link): Same as 5.
  8. CPython: Same code as 6.
  9. sphinx: Same problems as 5. and 6.

Summary: Two hits are due to a mypy bug, with a fix in progress. Apart from that all hits are true positives, of which a few may have real consequences. My overall assessment is that this PR is definitely worth it: Fairly low churn, which can catch potential real-life bugs. It will also prevent future accumulations of this problem.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant