How to prevent or detect short-circuit evaluation

I have operation where short circuit evaluation leads to very unpredictable results, that don’t produce exceptions:

The original implementation compares generator values and is very complex so I will omit it for now, but keep in mind that the __and__ method plays a crucial role and I need to be certain if is executed or not.

This code is an illustrative example of the kind short-circuit i meant:

class MyClass:
    def __init__(self, a ):
        self.value = a 

    def __le__(self, other):
        print("le")
        if isinstance(other, MyClass):
            return MyClass(self.value <= other.value)
        return MyClass(self.value <= other)
    
    def __ge__(self, other):
        print("ge")
        if isinstance(other, MyClass):
            return MyClass(self.value >= other.value)
        return MyClass(self.value >= other)
    
    def __and__(self, other):
        print("NOT IMPLEMENTED")
        return  4 
    
    def __rand__(self, other):
        print("NOT IMPLEMENTED")
        return  4 
    
    def __str__(self):
        return str(self.value)

# Example usage
a = 1
b = MyClass(2)
c = 3

print(a <= b <= c)    # __and__ not and called, hm...
print()
print((a <= b) and (b <= c))   # __and__ not being called, bad
print()
print((a <= b) & (b <= c))  #  __and__ is called, everything is fine

print()
print(type((a <= b)))
print(type((b <= c)))

As you can see __and__ is only called if is called with “&” that prevents the short circuit, but in the real use case is hard to know when __and__ is being skipped, and from a maintainability perspective it would be wise to raise an error, otherwise I may end up having short circuited structures giving wrong results.

Looking at numpy:

a<b<c of three arrays fails with an exception while (a<b) & (b<c) works just fine. In my case (a<b) & (b<c) works fine but a<b<c works as well, without an exception but with wrong values.

My question is if there a way to prevent or at least detect short circuit of a class? solely relying in not writing anything that can be short-circuited is hard to maintain and catch.

It seems to me that the exception raised in numpy for this caise is accidental but convenient, i need to have at least something similar, just imagine the mess that would have numpy if (a<b) and (b<c) would return something arbitrary.

I think you should redesign your code, and explicitly call whatever needs to be called.

Don’t override methods intended for Bitwise AND operations etc. (& etc) 6. Expressions — Python 3.13.0 documentation

in the naive hope of ‘short-circuiting’ the clearly defined and and or.

1 Like

There are valid reasons for this type of overload, I didn’t made it easy to judge as I didn’t provide the real code. but keep in mind that pandas and numpy made this things for years and they don’t need a redesign just for that, actually is a very convenient feature.

To be clear I don’t want or need short-circuiting, I want to avoid it, prevent it or at least catch it when is happening.

You’re already doing it; use &.

See support table:

As others pointed out, the function __and__ is called if the bitwise and operator (&) is used.

The operator and works differently: if bool(a) is True, then the expression a and b evaluates to b, otherwise, it evaluates to a. You cannot change that.

The chained comparization a<=b<=c is defined based on this: it evaluates like (a<=b) and (b<=c) (where b is only evaluated once).

If you do not want that your class is used as an operand to a and b, you can prevent it from being converted to a boolean, which can be done by defining the following member:

     def __bool__(self):
         raise TypeError("Cannot be converted to bool")

By adding this, your testcases yield:

print(a <= b <= c)    # TypeError
print((a <= b) and (b <= c))   # TypeError
print((a <= b) & (b <= c))  #  __and__ is called
5 Likes

@tstefan thanks! this information is exactly what I was looking for! a,b,c are not booleans and can’t be converted to them, same as numpy or pandas. Of course i would prefer that:

a <= b <= c  

would be read as:

(a <= b) & (b <= c)

But as I think it is impossible, so I can at least restrict the bad behaving case using your solution.

Many thanks again!

1 Like

You can use that as well:

class MyClass:
    def __init__(self, a):
        self.value = a

    def __le__(self, other):
        print("le", self, other)
        if isinstance(other, MyClass):
            return MyClass(self.value <= other.value)
        return MyClass(self.value <= other)

    def __ge__(self, other):
        print("ge", self, other)
        if isinstance(other, MyClass):
            return MyClass(self.value >= other.value)
        return MyClass(self.value >= other)

    def __lt__(self, other):
        print("lt", self, other)
        if isinstance(other, MyClass):
            return MyClass(self.value < other.value)
        return MyClass(self.value < other)

    def __gt__(self, other):
        print("gt", self, other)
        if isinstance(other, MyClass):
            return MyClass(self.value > other.value)
        return MyClass(self.value > other)

    def __and__(self, other):
        print("__and__ NOT IMPLEMENTED")
        return 4

    def __rand__(self, other):
        print("__rand__ NOT IMPLEMENTED")
        return 4

    def __str__(self):
        return str(self.value)



# Example usage
a = 1
b = MyClass(2)
c = 3

print(a <= b <= c) 

It is not relevant to avoiding short-circuiting, but MyClass(self.value >= other) creates an instance with the attribute value set to a boolean. That’s the cause of a <= b <= c not working—you are comparing booleans.

# Example usage
a = 1
b = MyClass(2)
c = 3

print(a <= b is True)  # False
print(b <= c is True)  # False

Tip: Remove __str__.