Thursday, June 4, 2026

How does __contains__ affect the behavior of the in operator?

All Questions From This Chapter    « Previously    Next »

The in operator checks whether an object is a member of a container (like a list, set, dict, or any custom class). Under the hood, it translates directly to a call to the container’s __contains__ method.

Here’s exactly how __contains__ affects that behaviour:


1. When __contains__ is defined

If a class defines the special method __contains__(self, item), the expression

python
Copy
Download
item in container

is equivalent to

python
Copy
Download
container.__contains__(item)

Python uses the truthiness of whatever __contains__ returns to decide the result:

  • Return Truein evaluates to True

  • Return Falsein evaluates to False

  • Return any other value → converted to bool (truthy = True, falsy = False)

python
Copy
Download
class AlwaysYes:
    def __contains__(self, item):
        return 1          # truthy → always True

class AlwaysNo:
    def __contains__(self, item):
        return 0          # falsy → always False

print(42 in AlwaysYes())   # True
print(42 in AlwaysNo())    # False

2. When __contains__ is not defined

If the class does not have __contains__, Python falls back in this order:

  1. Use __iter__ – iterate over the object and compare each yielded element to item using ==.

  2. Use __getitem__ – if no __iter__, but __getitem__ is defined, Python accesses indices 0, 1, 2, ... and compares each to item until an IndexError is raised (old‑style sequence protocol).

  3. If neither is available, a TypeError is raised.

This fallback is why you can write x in [1, 2, 3] or x in range(10) even though list and range don’t expose __contains__ directly – they rely on iteration.

3. Custom membership logic

By overriding __contains__ you can completely redefine what “membership” means for your objects. For example, a fuzzy string matcher:

python
Copy
Download
class FuzzyStr:
    def __init__(self, text):
        self.text = text
    def __contains__(self, sub):
        return sub.lower() in self.text.lower()

s = FuzzyStr("Hello World")
print("hello" in s)   # True – case‑insensitive match

4. Important notes

  • __contains__ is only called on the right‑hand object (container). The left‑hand item can be anything; no special method is required on it.

  • The not in operator simply negates the result of __contains__ (or its fallback).

  • For dictionaries, key in d calls dict.__contains__, which looks for the key, not the value.

  • If __contains__ explicitly returns a non‑boolean, the standard truth‑testing rules apply (e.g., 0, None, "", empty containers are falsy; everything else is truthy). This can lead to surprising results if you accidentally return a value like None, so it’s best to always explicitly return a bool.

In short: __contains__ replaces the default iterative search with whatever custom logic you define, giving you full control over the in operator.


Note: If __contains__ returns None, the in operator will evaluate to False. This happens because in uses the truthiness of the return value, and None is falsy.


All Questions From This Chapter    « Previously    Next »

No comments:

Post a Comment