PYTHON PROGRAMMING
An OptionalBool Type for Python: None, False or True
Use OptionalBool instead of Optional[bool].
Among the various built-in types we have in Python are the NoneType
and the bool
types. Incredibly useful and extremely common, both are used in almost all Python projects — if not all. Who doesn’t use None
, True
and False
values in Python?
Sometimes, however, we need to combine these three values within one variable: Optional[bool]
. As this type hint suggests, this means you can use a bool
value for this variable — but don’t have to. The Optional
option often helps in handling optional arguments of functions, but I’m not talking about this situation. I am talking about different situations, once in which the None
value carries a particular piece of information. Best to show how this works using an example.
Consider the following situation. Given a list of paths, you check if a path has a .txt
extension; if it does, you follow with a check if it is in English. For a particular path, we can encode these three situations as the following values:
None
: the file is not*.txt
False
: the file has the.txt
extension but is not in EnglishTrue
: the file has the.txt
extension and is in English
As you see, such a variable has the Optional[bool]
type hint. Python does not offer a built-in data type for such a variable. We can use the Optional[bool]
annotation itself; I will discuss this topic later in this article. But this means merely using type hint not a data type (like bool
). A type hint is just a hint while a data type is a data type — the former only suggests while the latter requires. As you will see, a data type of this sort can come in handy in various situations, one of them being the variable described above.
To represent it, we could use a two-step bool
value; for example, it could be represented by tuple[bool, bool]
. Let’s see how such a tuple would work instead of the above-defined three-value type:
(False, None)
: the equivalent ofNone
→ the file is not*.txt
(True, False)
: the equivalent ofFalse
→ the file has the.txt
extension but is not in English(True, True)
: the equivalent ofTrue
: the file has the.txt
extension and is in English
Such a tuple is far more complicated than a three-value data type — even if we use a named tuple. First, we have two bool
values, and this is no fan. Even worse, the meaning of None
is unnatural — it means “it is not available, but it does not matter.” Do you see why? Because the user can use (False, True)
and (False, False)
— and both will carry exactly the same meaning as (False, None)
. Do you see that it really does not matter what’s the second value of the tuple? I don’t like such it-does-not-matter situations.
There are crude ways to achieve something like this with one variable of a built-in type. For example, you can use a string type and give it one of the following values:
"None"
, the equivalent ofNone
→ the file is not*.txt
."False"
: the equivalent ofFalse
→ the file has the.txt
extension but is not in English"True"
: the equivalent ofTrue
: the file has the.txt
extension and is in English
In my opinion, this solution is pretty nasty, something I’ve never used and most likely never will.
In this article, I will show you two solutions, both likely being better than using a string variable. The first of them will be very simple but at the same time a little clumsy to use; this solution will be based on a type hint. I don’t know whether or not it is better than using a string variable, as I would use neither of these solutions.
I’d use something else instead — a dedicated custom data type and at the same time, a custom data structure. In my other article, I wrote that a custom data structure is better than a type alias:
It is so because you get two things in one: both a data type and a data structure. I present such a data type to be in our situation as the second version, and I consider it much more useful and pleasant to use.
Approach 1: Use type alias
You can approach the problem in a simple way. Define the following custom type alias: Optional[bool]
and use a variable of this type.
The solution may sound elegant, probably thanks to its brevity. But it’s definitely not such. It will work, but from a coding point of view, it will likely be unpleasant to use, especially in a big coding project. One of the main problems is a complete lack of protection — a variable of such a type can be given any value, including illegal ones — and you will not know this. This can lead to unpleasant errors, sometimes difficult to debug. Worst-case scenario, no error would be thrown whatsoever and the application would run despite the incorrect value.
You can circumvent such errors by adding value checks, for example using the easycheck
package:
Value checks are fine in some situations, but when there’s a way to avoid them, it’s better to do it.
While this solution is slightly neater than using a string variable, it’s still neither elegant nor convenient. It’s easy to see this inconvenience. Note that if you want to use several variables of this type, you need to annotate each of them in each scope. Otherwise, it will be a regular Python variable, one that can take any value. Surely, variables of this type can be assigned anything, but their type alias at least points out what sort of values should be used. Should be, not must be.
Okay, let’s check how this solution could work.
>>> from typing import Optional
>>> NoneOrBool = Optional[bool]
>>> true: NoneOrBool = True
>>> false: NoneOrBool = False
>>> none: NoneOrBool = None
>>> true
True
>>> true is True
True
>>> true is None
False
>>> if true: print("This is true!")
This is true!
>>> none is None
True
>>> type(none).__name__
'NoneType'
>>> none
>>> false
False
(I used the name NoneOrBool
only because I will use the name OptionalBool
later, and I wanted to avoid using the same name for the two solutions.)
This is all fine and works as expected. However, the above code block shows only correct uses of the NoneOrBool
type. It takes almost nothing to assign an unacceptable value to a variable of the NoneOrBool
type:
>>> nonsense: NoneOrBool = "Nonsense"
Generally, this is how Python works. This is dynamic typing. It disables us to use the typical approach to if
comparisons that we used above, because:
>>> if nonsense: print("This is true!")
This is true!
As you see, you can annotate nonsense
as a NoneOrBool
variable, but it does not mean that this variable will not have a different type — dynamic typing! The if
comparison I’ve shown above makes this variable clumsy and incoherent with its annotated type.
So, anytime we use this variable, and you want to ensure that it does have the NoneOrBool
type, we need to check it. This is why I prefer the following solution: using a custom data structure.
Approach 2: OptionalBool custom data structure
At first, I was thinking of making OptionalBool
an abstract base class (ABC). With a different name, you could make it a type with three values of the user’s choice. After some pondering, however, I decided not to. Just like we have NoneType
and the bool
type, we need an OptionalBool
type, with the three possible values of None
, False
and True
. And if you need a different type with three possible values, you can create your own class.
Here’s the definition of OptionalBool
:
# optionalbool.py
from typing import Any, Optional
class OptionalBool:
def __init__(self, value: Optional[bool] = None):
if value is not None:
self._check_value(value)
self._value = value
@staticmethod
def _check_value(value: Optional[bool] = None):
msg = "OptionalBool accepts only None, True, False values"
if value is not None:
if value not in (None, True, False):
raise ValueError(msg)
@property
def value(self):
return self._value
@value.setter
def value(self, new_value: Optional[bool]):
self._check_value(new_value)
self._value = new_value
def __repr__(self):
return f"{self._value}"
def __eq__(self, other: Any):
if other is None:
return self.value is None
if hasattr(other, "value"):
return self.value == other.value
return self.value == other
The class is organized in such a way that you will rarely need to use its .value
attribute.
This is how OptionalBool
works. First, let’s create two OptionalBool
values, x
and y
.
>>> from optionalbool import OptionalBool
>>> x = OptionalBool()
>>> x
None
>>> y = OptionalBool(True)
>>> y
True
Easy, nothing special — which is good. What we need for this type is comparisons. OptionalBool
’s instances have their value, which can be None
, False
or True
. This value is kept by the .value
attribute, and you can use it:
>>> x.value
>>> if not x.value:
... print(f"{x.value = }")
x.value = None
>>> x.value is None
True
>>> if y.value:
... print(f"{y.value = }")
y.value True
Unfortunately, Python does not enable us to overload the is
operator, so we will have to adopt to this situation. Here are the rules for making comparisons of OptionalBool
instances. You have to follow these (simple) rules if you want your comparisons to be correct:
- In no case should you compare an
OptionalBool
instance using theis
operator; this may lead to incorrect conclusions. - The only valid comparisons between two
OptionalBool
instances are those using the==
operator (that is, the comparisons using.__eq__()
). - For a particular instance of
OptionalBool
, only the following comparisons can be positive with another object (using==
): anOptionalBool
instance with anotherOptionalBool
instance with the same value;OptionalBool(None)
withNone
;OptionalBool(True)
withTrue
; andOptionalBool(False)
withFalse
.
Okay, first some simple comparisons, with another value of OptionalBool
type:
>>> x == y
False
>>> x != y
True
>>> x < y
Traceback (most recent call last):
...
TypeError: '<' not supported between instances of 'OptionalBool' and 'OptionalBool'
>>> x >= y
Traceback (most recent call last):
...
TypeError: '>=' not supported between instances of 'OptionalBool' and 'OptionalBool'
Do remember, however, that such comparisons would behave differently when using the .value
attribute of the instances. That’s why if you want to compare two OptionalBool
instances, you should not do that using this attribute but the instances themselves, like above.
You can also use the .value
attribute for these comparisons, and they will lead the same results. If you look into the implementation of comparisons (.__eq__()
), you will see why: because the comparisons of instances (e.g., x == y
) use the .value
attribute. So, basically, when we run x == y
, in fact another comparison is run: x.value == y.value
.
When being compared to regular bool
values, we want OptionalBool
values to behave like bool
. As mentioned above, we cannot overload the is
operator. Therefore, typical comparisons with instances will not work:
>>> true = OptionalBool(True)
>>> false = OptionalBool(False)
>>> none = OptionalBool()
>>> if true: print("Yup")
Yup
>>> if false: print("Yup")
Yup
>>> if none: print("Yup")
Yup
Thus, here we should use .value
:
>>> if true.value: print("Yup")
Yup
>>> if false.value print("Yup")
>>> if none.value print("Yup")
>>> true.value is True
True
>>> false.value is False
True
>>> none.value is None
True
Let’s look once more at the comparisons of none
(the OptionalBool
instance created above) with None
:
>>> none == None
True
>>> none is None
False
>>> none.value is None
True
Comparisons with bool
values work the same way:
>>> true is True
False
>>> true == True
True
>>> false is False
False
>>> false == False
True
>>> false.value == False
True
>>> false.value == True
False
>>> false.value is None
False
>>> false.value == None
False
>>> false.value == 10
False
A good thing about OptionalBool
is that you can create its instance only with its accepted values: None
, False
and True
:
>>> OptionalBool("nonsense")
Traceback (most recent call last):
...
ValueError: OptionalBool accepts only None, True, False values
Along the same lines, you cannot assign an unacceptable value to an OptionalBool
instance:
>>> x = OptionalBool(True)
>>> x
True
>>> x.value = "nonsense"
Traceback (most recent call last):
...
ValueError: OptionalBool accepts only None, True, False values
In short, comparisons of OptionalBool
instances work as follows:
- In no case should you compare (with nothing) an
OptionalBool
instance using theis
operator; this may lead to incorrect conclusions. The best idea is to forget theis
operator forOptionalBool
. - The only valid comparisons between two
OptionalBool
instances are those using the==
operator. - Comparison of
OptionalBool
instances is not equivalent of comparisons ofOptionalBool
values (OptionalBool.value
). - For a particular instance of
OptionalBool
, only two objects are installed that are positive when compared to this instance: another instance with the same value and the value of theNone
orbool
object is the same as the value of the instance.
Conclusion
Why at all have I proposed OptionalBool
? For the simple reason that a type like this can be useful in various real life situations in which we need a variable that takes one of three possible values: None
, False
or True
.
I presented two solutions. The first one was just a custom type, but as such, it does not offer any particular convenience. The second one was an actual data structure, built as a class. Although I offered its particular implementation, to be honest, my main goal was to present the concept of OptionalBool
and outline its general functionality. Maybe some of you will need a type like this, and we all know that necessity is the mother of invention. Feel invited to use the implementation proposed in this article; if you come up with another interesting implementation, however, please share it — either in your article or as a comment to this one.
I initially planned to include also a third method that would be based upon the Enum
enumeration:
I thought Enum
would make things much simpler. I started implementing this type, and after some time it occurred to not be simpler than OptionalBool
. This is because I decided to handle various operators and comparisons. For instance, I wanted to make it possible to conduct the following comparison: x == True
, where x
is an OptionalBool
instance. As we discussed above, this is possible in the above implementation, but to achieve the same functionality in an enumeration class inheriting from Enum
, we would have to overload the .__eq__()
method anyway. Hence, the complexity of both implementation would be close. Hence I did not implement OptionalBool
inheriting on Enum
but left it to you as an exercise. Who knows, maybe it will be a better implementation?
Let me finish this article by outlining the OptionalBool
class:
OptionalBool
allows for one of the three possible values:None
,False
andTrue
.OptionalBool(True)
should be equal toTrue
;OptionalBool(False)
should be equal toFalse
; andOptionalBool(None)
should be equal toNone
.OptionalBool
should be both a data structure and a data type. Hence,type(x)
whenx
is an instance ofOptionalBool
should provideOptionalBool
.OptionalBool
instances should not be compared using theis
operator. Their values, however, can. Hence, you should not comparex is y
, wherex
andy
areOptionalBool
instances; but you can comparex.value is y.value
.OptionalBool
instances should be compared using the==
operator only. Although you can use other comparison operators forbool
values, you should disallow this forOptionalBool
instances and values, as they do not make sense for abool
value and aNoneType
value.
Thanks for reading. If you enjoyed this article, you may also enjoy other articles I wrote; you will see them here. And if you want to join Medium, please use my referral link below: