Is there a road towards making bool a fully-fledged Boolean type?

This has been something I’ve wanted for a long time and seeing all the support on Paul’s suggestion here, I was wondering if there is any possibility towards making progress towards making bool a non-integer in the long run.

In particular, are any of these feasible in the short or long run?

  • making bool incompatible with other arithmetic operations like +,-,*,/,%,|,&,^
  • making int not imply int | bool in type annotations
  • making bool not be a subclass of either int nor numbers.Integral
  • any others?

The argument for such a change would be that these arithmetic operations:

  • do not coincide with how numerical libraries treat Boolean values, (e.g., True + True == 2, but the Array API will treat this as or),
  • exhibit some footguns (e.g., True & 4 == 0 whereas True and 4 == 4),
  • are rarely used,
  • and their old behaviour can easily be recovered with an explicit cast to int.

Also, the smaller the bool interface, the more effective static type checkers are at discovering logical errors (for example, a Boolean flag that leaked into an arithmetic operations by accident).

I realize this is far smaller priority than the ~bool deprecation and more disruptive. I’m more curious about how this balances out.

7 Likes

I have used +, -, * quite regularly with booleans. I would be sad to see such behaviour lost.
I’m not aware of tidier alternatives for for example

x += 4 * condition

and

sum(test(i) for i in range(N))

(edit: that first one does have if condition: x += 4 as an alternative, but I don’t think the point is entirely invalid. 4 * condition is shorthand for (4 if condition else 0) and more readable imo)

8 Likes

It would be too distruptive. Too many code depends on bool be compatible with int. Some examples from the stdlib code:

  • d = "rf"[self.isearch_direction == ISEARCH_DIRECTION_FORWARDS]
    
    Could be written as
    d = "f" if self.isearch_direction == ISEARCH_DIRECTION_FORWARDS else "r"
    
  • return sum(value == entry for entry in self)
    
    Could be written as
    return sum(1 for entry in self if value == entry)
    
  • ndays = mdays[month] + (month == FEBRUARY and isleap(year))
    
    Could be written as
    ndays = mdays[month]
    if month == FEBRUARY and isleap(year):
        ndays += 1
    

Such code is not buggy or error-prone, but you should rewrite it if bool is less like int. The only painless way to do this is to borrow a time machine, travel 34 years ago, and convince Guido that it needs a distinct boolean type (it was before long integers, ternary operator, iterators and generators and bytecode).

14 Likes

All of these would still be functional by having an extra call to int, and the first one should actually be functional anyway since the new bool can just implement __index__.

1 Like

To be clear, I agree with @storchaka here, in spite of my post being quoted as the motivation for this idea. The point I was making was that I didn’t see the benefit of just changing one thing about bool - not that I supported an overhaul of behaviour that’s been around and served us just fine for over 30 years…

If anyone is genuinely serious about this proposal, they’ll need to justify the breakage that would be caused by changing something this basic that’s been in the language for so long. And “you just have to add in an extra call to int” won’t cut it - who’s volunteering to go through the many millions of lines of Python code in existence, a significant portion of which isn’t public, and much of which supports critical business logic, and make all those changes?

10 Likes

The simple answer to any question of this form is:

x += 4 * int(condition)

Personally I would write:

if condition:
   x += 4

I prefer this because it makes it clear that x is not being modified when condition is False and I don’t want to actually execute the * and the + if that is the case.

In the other thread I suggests that a non-int bool type could still support arithmetic operations although others disliked that idea. The reason I suggested it is because while changing isinstance(True, int) is a compatibility break, the fact is that 99.9% (made up number) of the breakage would come from breaking arithmetic. Also while some people might not like arithmetic with booleans it is at least unambiguous and is done through the explicit use of arithmetic operators. It can easily just fall into the category of things that are possible but that you prefer not to use in your own code (as it already does for me). I assume that the original motivation for making bool a subclass of int came from wanting to avoid breaking arithmetic with conditions like this.

3 Likes

We change potentially dangerous things that are not in large use and keep useful and harmless things. I think this is a right approach.

5 Likes

Agree, but there is still 0.1% of code (mostly different kind of serializers and dispatchers) that depends on bool been a subclass of int. This may be not a large issue. But why? Why spend so much effort to imitate current behavior if the current implementation works pretty well? There is no large issue to be solved.

4 Likes

More inspiration than motivation! I just thought the other thread and your comment had a lot of worthwhile background reading for this thread, which people responding might want to read. Hope you didn’t feel I was pinning this idea on you :smile:

Regardless, I appreciate all of the answers I’ve received so far. I always learn a lot.

In general, I’m very idealistic, and I live (and propose ideas) as if I had that time machine that Serhiy was talking about :laughing:

1 Like
  • Multiplying a collection by a boolean is quite useful.

    For example, it can handle whether a specific string should be included or not.

    number = 2
    # There are 2 apples.
    print(f"There are {number} apple{'s' * (number > 1)}.")
    

    It is also useful when deciding whether to include a specific element in a collection.

    extensive = True
    cases = (1, 2, 3, 4) + (5, 6) * extensive
    
  • XOR has no corresponding boolean operator, thus the bitwise ^ is the only method available[1].

  • Other bitwise operations are used in ‘eager operation’.

    For example, bool1 or bool2 does not evaluate bool2 if bool1 is True, whereas bool1 | bool2 evaluates bool2 regardless of the value of bool1.


  1. You could use bool1 + bool2 == 1, but… That’s another operation, isn’t it? ↩︎

1 Like

Just FYI, many collections don’t support multiplication by integers or Booleans. set is a collection. Some sequences (tuples, lists, and strings, e.g.) do support it. I think it doesn’t hurt to cast to integer or using a ternary for this case for the sake of readers, but I understand the appeal of multiplying by a Boolean.

I would use x != y personally since it conceptually works with a simple mental model that relies on Boolean values only. You don’t have to imagine that the Boolean values are standing in for integers like you do with ^ or x+y==1.

I think this would be extremely confusing to do to a reader of code without an accompanying comment. That would open up the potential of subtle bugs if someone were ever to refactor the code to use Boolean operators or some other way. If you really need both branches to be evaluated, then evaluate them on separate lines with a comment saying that the side effects are necessary. Then use the appropriate Boolean operator.

2 Likes

That doesn’t require bool be a subclass of int, just mappable to int. You could still write

print(f"There are {number} apple{'s' * int(number > 1)}.")

with the same semantics, but I would argue that even now

print(f"There are {number} apple{'s' if number > 1 else '')}.")

is clearer.

4 Likes

I would argue that all of these should use number != 1 to properly handle the zero-apple scenario. :slightly_smiling_face:

Oh, and also that use of “are”…

1 Like

It seems like the easy solution to the mathematical issues is to auto-promote bools to ints when those operations are attempted.

1 Like

Isn’t that what already happens?

In [**1**]: x = **True**

In [**2**]: x += 5

In [**3**]: x

Out[**3**]: 6

In [**4**]: type(x)

Out[**4**]: int

Maybe I don’t know what auto-promote means?

Is it:

In [15]: b = True

In [16]: isinstance(b, int)
Out[16]: False

but you could still do math with them?

Which I suppose would help the static typing folks, but I’m a typing-skeptic, so I don’t have an opinion about that.

It is. We could maintain that behavior even if bool was not a subclass of int.

1 Like

I would restrict it to +, - and * or in dunder terms pos, neg, add, radd, mul and rmul. These are the only operations where it is useful to use a bool as an int and could be something like:

def __add__(self, other):
    return int(self) + other

That would then work for multiplying int, list etc.

Other arithmetic operations like **, /, % could be disallowed although possibly << is reasonable. You could also disallow mixing bool and int in bitwise binary operators like 2 & True while still allowing &, |, ^ when both operands are bools (and of course having ~ work like not).

1 Like

If we go this way (unlikely), could we also consider (or at least not rule out) extension to 3-state logic as a Boolean subclass?

Can we start with reverting the decision on True and False being introduced in Python 2.2.1? :stuck_out_tongue_winking_eye: /me ducks!

Less disruptively, static type checkers could behave as if they were independent types regardless of how they’re actually implemented.

So CPython would keep assert issubclass(bool, int) as an implementation detail, but formally bool would be its own type that was only situationally usable as an integer without an explicit cast.

6 Likes