Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Set operations: should only work with sets, but works with dict_keys?

The documentation for set operations says:

Note, the non-operator versions of union(), intersection(), difference(), symmetric_difference(), issubset(), and issuperset() methods will accept any iterable as an argument. In contrast, their operator based counterparts require their arguments to be sets. This precludes error-prone constructions like set('abc') & 'cbs' in favor of the more readable set('abc').intersection('cbs').

Testing this with the following experiment:

# Python 3.10.2 (main, Jan 15 2022, 19:56:27) [GCC 11.1.0] on linux

>>> set('ab') & set('ac')
{'a'}
# works, as expected

>>> set('ab') & 'ac'
TypeError: unsupported operand type(s) for &: 'set' and 'str'
# doesn't work, as expected

>>> set('ab') & list('ac')
TypeError: unsupported operand type(s) for &: 'set' and 'list'
# doesn't work, as expected

>>> set('ab') & iter('ac')
TypeError: unsupported operand type(s) for &: 'set' and 'str_iterator'
# doesn't work, as expected

>>> set('ab') & dict(zip('ac', 'ac')).keys()
{'a'}
# works??

>>> type({}.keys())
<class 'dict_keys'>

>>> isinstance({}.keys(), (set, frozenset))
False

So, here is the paradox:

  • set operator & works with dict_keys objects;
  • The documentation says it should only work with sets;
  • dict_keys objects are not sets.

Why does set operator & work with dict_keys objects? Are there other types that it works with? How can I find a list of these types?

like image 980
Stef Avatar asked Oct 18 '25 14:10

Stef


2 Answers

this is not a complete answer, but dict_keys are instances of collections.abc.Set:

from collections.abc import Set
k = dict(zip('ac', 'ac')).keys()
print(isinstance(k, Set))  # -> True
like image 192
hiro protagonist Avatar answered Oct 21 '25 11:10

hiro protagonist


This section of the docs is referring to what the set/frozenset types support. Indeed, set does not support the comparison with dict_keys instances, and it correctly returns NotImplemented to indicate this operation is unsupported:

>>> left = set('ab')
>>> right = dict(zip('ac', 'ac')).keys()
>>> left.__and__(right)
NotImplemented

This return value indicates that Python should attempt the reflected operation with the other operand, to see if that is supported. And it succeeds:

>>> right.__rand__(left)
{'a'}

If both type(left).__and__ and type(right).__rand__ are returning NotImplemented, then you will see a TypeError exception.

So, there is no paradox, just a subtlety in the datamodel: after set opts out, the dict_keys type is afforded the chance to handle the binary operation from the right-hand side.

like image 35
wim Avatar answered Oct 21 '25 09:10

wim



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!