Friday, June 5, 2026

How does Python determine the truth value of an object if __bool__ is not implemented?

All Questions From This Chapter    « Previously    Next »

When Python needs to determine if an object is “truthy” or “falsy” (for example in an if statement or bool(obj)), it follows a specific fallback order:

  1. __bool__ – Python first looks for a __bool__ method on the object. If present, it calls that method and uses the boolean result directly.

  2. __len__ – If __bool__ is not implemented, Python next looks for a __len__ method. If that exists, it calls __len__() and treats the object as True if the result is non-zero, and False if the result is 0.

  3. Default True – If neither __bool__ nor __len__ is defined, the object is considered True by default. (This applies to most user-defined class instances unless they explicitly define one of the two methods.)

Thus, the truth value is False only if:

  • The object defines __bool__ and it returns False, or

  • The object does not define __bool__ but defines __len__ and __len__() returns 0.

Otherwise, it is True.

Example with a custom class

python
Copy
Download
class AlwaysTrue:
    pass                    # no __bool__, no __len__

class EmptyContainer:
    def __len__(self):
        return 0

class NonEmptyContainer:
    def __len__(self):
        return 5

class ExplicitFalse:
    def __bool__(self):
        return False

# Testing
print(bool(AlwaysTrue()))       # True (default)
print(bool(EmptyContainer()))   # False (__len__ returns 0)
print(bool(NonEmptyContainer()))# True (__len__ returns 5)
print(bool(ExplicitFalse()))    # False (__bool__ returns False)

Why this order?

It allows built-in types like lists, strings, and dicts to be “falsy” when empty without needing a special __bool__. They just implement __len__:

python
Copy
Download
bool([])      # False – list.__len__ returns 0
bool([1,2])   # True  – list.__len__ returns 2

Objects that want to be unconditionally True can leave both methods undefined. If you want to make an object explicitly falsy, define __bool__ to return False.

This mechanism is defined in Python’s Truth Value Testing rules.


All Questions From This Chapter    « Previously    Next »

What is the difference between __mul__ and __rmul__?

All Questions From This Chapter    « Previously    Next »

In Python, __mul__ and __rmul__ are both used for the * operator, but they are called in different situations.

__mul__(self, other) is tried when your object is on the left side:

Python
a * b

__rmul__(self, other) is tried when your object is on the right side and the left object does not know how to multiply with it:

Python
b * a

Example:

Python
class Box:
def __mul__(self, other):
return "Box __mul__"

def __rmul__(self, other):
return "Box __rmul__"

x = Box()

print(x * 3) # calls __mul__
print(3 * x) # calls __rmul__

Python’s rule is roughly:

  1. Try left operand’s __mul__

  2. If it returns NotImplemented, try right operand’s __rmul__

This is especially useful for supporting expressions where your custom class appears on the right side of a built-in type, like 2 * custom_object.


All Questions From This Chapter    « Previously    Next »

Thursday, June 4, 2026

Why should you avoid directly calling special methods like __len__?

All Questions From This Chapter    « Previously    Next »

You should avoid calling special methods like __len__ directly because the built‑in functions and operators that invoke them (like len(), in, str(), etc.) are the intended public API. They often do extra work that you’d lose by calling the dunder method yourself.


1. Fallback behaviour is lost

Many operations have a fallback mechanism that a direct __ call bypasses.

For example, item in container works even when __contains__ is not defined, because in will fall back to iteration via __iter__ or __getitem__. If you write container.__contains__(item) directly, you get an AttributeError:

python
Copy
Download
class MyContainer:
    def __iter__(self):
        yield 1
        yield 2
        yield 3

obj = MyContainer()

print(2 in obj)            # True  (uses iteration fallback)
print(obj.__contains__(2)) # AttributeError: 'MyContainer' object has no attribute '__contains__'

The same principle applies elsewhere. The built‑ins handle the complexity so you don’t have to.


2. Type checks and validation are bypassed

Built‑in functions often verify the return value of a special method. len() for example ensures that __len__ returns an int ≥ 0, and raises a clear TypeError otherwise.
Calling obj.__len__() directly skips that check, potentially giving you a weird value that causes confusing bugs later.

python
Copy
Download
class BadLen:
    def __len__(self):
        return "oops"

b = BadLen()
len(b)             # TypeError: 'str' object cannot be interpreted as an integer
b.__len__()        # "oops"   (no immediate error – trouble down the line)

3. Readability and convention

len(book) reads naturally.
book.__len__() is an implementation detail leaking into your code.

Special methods exist for the interpreter, not for direct human use. They’re a contract the object fulfills so that the language can treat it like a standard sequence, mapping, etc. Using the public function makes your intent clearer and keeps your code “Pythonic”.


4. Consistency across types

Not every object has a __len__ method, but len() gives a consistent experience (it raises a uniform TypeError). Direct calls would give AttributeError on objects without the method, breaking polymorphic code that expects a TypeError.


Bottom line:
Just as you write item in container instead of container.__contains__(item), always use len(obj) instead of obj.__len__(), str(obj) instead of obj.__str__(), and so on. Let Python’s built‑in machinery do its job.


All Questions From This Chapter    « Previously    Next »

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 »