`list() - dict().items()` returns `set()`?

current scenario -

  1. if we subtract an object of type dict_items from an object of type list then it returns a set
  2. based on my inspection, there are only two types that one could subtract from a list, dict_items, and dict_keys.
    example,
    both,
[1] - dict({'a': 1}).items()

and,

[1] - dict({'a': 1}).keys()

give,

{1}

expected scenario -

  1. this subtraction is made invalid, just like list() - list() or list() - set() is invalid.

note -
similar pattern is also observed if we use a str, tuple instead of a list
that is,

'abc' - dict({'a': 1}).keys()

gives the set,

{'b', 'c'}

I think this should be made invalid, just like, 'abc' - 'a' is invalid.

1 Like

Welcome to the wonderful world of operator overloading!

When you use an operator, like -, both operands are checked to see if they support the operator with the other argument.

So when you do mylist - mydict.items(), the interpreter checks:

  • Do lists support subtracting other objects? No.
  • Do dict itemview objects support being subtracted from other objects? Yes.

And so the operation succeeds.

Dict itemview objects support a “set-like” interface, which includes subtraction, and so the subtraction succeeds.

Likewise for all the other examples. Keyview and itemview objects accept any sort of sequence or set as the other operand for set union, intersection, difference etc.

4 Likes

but shouldn’t subtraction return some object that is a subset of the first argument, for example,

{1, 2, 3} - {1}

would give,

{2, 3}

which is a subset of {1, 2, 3}, but for list() - dict().items(), subtraction returns an object of a new type, shouldn’t it be a subset of the first argument, which is a list(), and a subset of a list() does not mean anything, as subtraction is not valid on a list, so the subtraction list() - dict().items() should also be invalid.

By via Discussions on Python.org at 03Jun2022 15:48:

but shouldn’t subtraction return some object that is a subset of the
first argument, for example,

{1, 2, 3} - {1}

would give,

{2, 3}

which is a subset of {1, 2, 3}, but for list() - dict().items(),
subtraction returns an object of a new type, shouldn’t it be a subset
of the first argument, which is a list(), and a subset of a list()
does not mean anything, as subtraction is not valid on a list, so the
subtraction list() - dict().items() should also be invalid.

Well, no.

The implementors of itemview wanted subtraction to be a meaningful idea.
Since subtraction between lists is not supported (we could argue for
days about what that should mean, if it were supported) they chose to
use a set operation for subtraction. This has the advantage that it can
be efficient, since a set membership test is O(1).

The result of a set operation is naturally a set.

Have a read of this:

which explains some of the rationale, and why the views are considered
set-like, and thus the operations return sets.

If you think of subtraction as a function instead of an operator,
consider that many functions return values of a different type from
their operands. For a particular type there will be a suite of functions
which return values in the same domain (same type), but also some
functions which return values in a new domain, or of a different type.

You’ve got subtraction between a list and a dict item view. These are
different types already. If another list is not a reasonable result (or,
if not unreasonable, just not the desired or preferred kind of
result), and the operation can’t really return another itemview (because
such a thing is inherently a view of a dict), a “set” result is useful
and reasonable. Particularly since it can immediately be used with other
sets!

Cheers,
Cameron Simpson cs@cskk.id.au

1 Like

Many operations coerce their arguments to a common type, which may not necessarily even be the type of either operand.

There is nothing wrong with an operator accepting objects by their interface, not their type, such as this:

(set-like object) - (any iterable) -> set
(any iterable) - (set-like object) -> set

Of course, any language that allows operator overloading has to come up with a rule for deciding which operand wins. Python’s rule is complex, and I may have some details wrong, but I believe it works like this:

  • Each operator corresponds to a pair of dunder methods, a regular method and a “reflected” method; e.g. for subtraction they are __sub__ and __rsub__.
  • If an object doesn’t support the operation at all, it should just not define those two dunders.
  • If it supports the operation for only some arguments, it should return NotImplemented to signal that the object does not understand how to perform the operation.
  • First object to return any other return result, or raise an exception, wins.
  • The standard behaviour is to call the left-hand object’s __sub__ method first, and if that is missing or returns NotImplemented, try the right-hand object’s __rsub__ method.
  • If both methods are missing or return NotImplemented, the default behaviour is to raise TypeError.

There are two exceptional cases:

  • If both objects are the exact same type, then the reflected method isn’t tried.
  • If the right-hand object is a subclass of the left-hand object, then its reflected method is tried first.

Comparison operators have a slightly different set of names for reflected dunders, e.g. __gt__ and __lt__ are reflections of each other.

1 Like

shouldn’t even sets work here, that is,

[1] - {1}

should give,

set()

but instead it gives,

TypeError: unsupported operand type(s) for -: 'list' and 'set'

I think @steven.daprano intended like,

(set-like *dict-view* object) - (any iterable) -> set
(any iterable) - (set-like *dict-view* object) -> set