Nick S
Nick S

Reputation: 621

Why does the key received by `__getitem__` become `0`?

I was implementing a __getitem__ method for a class and found that obj[key] worked as expected, but key in obj always transformed key into 0:

class Mapper:
  def __getitem__(self, key):
    print(f'Retrieving {key!r}')
    if key == 'a':
      return 1
    else:
      raise KeyError('This only contains a')
>>> mapper['a']
Retrieving 'a'
1
>>> 'a' in mapper
Retrieving 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in __getitem__
KeyError: 'This only contains a'

I didn't find a __hasitem__ method, so I thought the in check worked by just calling __getitem__ and checking if it throws a KeyError. I couldn't figure out how the key gets transformed into an integer, of all things!

I couldn't find an answer here, so I started writing this question. I figured out the answer before I posted, but in the interest of saving other people some time, I'll post my question and solution.

Upvotes: 1

Views: 341

Answers (1)

Nick S
Nick S

Reputation: 621

It's been a while, so I totally forgot the method I was looking for is called __contains__, not __hasitem__!

What's more, the fallback isn't the same as other, similar dunder methods in Python! Usually I'd expect that if __contains__ is missing, it would just use __getitem__. Instead, the in syntax uses a special series of fallbacks:

  1. If __contains__ exists, use that.
  2. Else, if __iter__ exists, use it to iterate over the items in the object and check if any of them match the key.
  3. Else, if __getitem__ exists, use it to iterate over the items in the object as if it were a sequence (e.g. a list): give __getitem__ every integer, starting at 0, and either stop when it throws an IndexError or it returns something matching the key.

In my case, I was raising a KeyError when __getitem__ received 0, which got passed up to the caller.

Upvotes: 3

Related Questions