There are six built-in Python constants. Most developers become familiar with True, False, and None as soon as they begin working with the language, but the other three (NotImplemented, Ellipsis, and __debug__) are less common. So today, I'm going to share fun tidbits about each of them.

True and False

Mutability

These are the two boolean constants. Though both were once upon a time mutable in Python 2, this changed in Python 3 to prevent incorrect logic. Check it out!

The previous Python 2 definition for False:

The false value of the bool type. New in version 2.3.

And an example of the wacky logic that came with this mutability:

False = True

True == False      # True?!
True is False      # True?!

(1 == 1) is False  # True?!
(1 == 2) is False  # False?!

Thankfully, when Python 3 was released, the definitions were updated. This is the new False definition:

The false value of the bool type. Assignments to False are illegal and raise a SyntaxError.

And true to its word, the True/False constants are no longer modifiable:

False = True
    File "<stdin>", line 1
      False = True
      ^
  SyntaxError: cannot assign to False

Customizable

Most Python objects are truthy. But if this behaviour doesn't suit your needs, Python allows you to customize the boolean evaluation of any object using the __bool__ magic method (__nonzero__ for Python 2.6 and below). Here's a simple example:

class Foo:
    def __init__(self, x):
        self.x = x

    def __bool__(self):
        return bool(self.x)
        
bool(Foo(4))  # True
bool(Foo(0))  # False

Without a __bool__ defined, both of those Foo instances would have evaluated to True instead!

In the process of this picture I accidentally fumigated my whole bedroom. Next time I should definitely do this outside...

None

This is the ultimate null value, and it is the only instance of the NoneType class allowed to exist. A singleton.

None often causes confusion in boolean contexts, because even though None is the null singleton, Python also evaluates many other expressions to False. Values like 0, [], {}, and set() are all falsy. Said another way, if foo is not the same as if foo is not None. Make sure to differentiate between empty objects and None.

NotImplemented

This is another singleton, the only instance of NotImplementedType.

There's also a NotImplementedError, but it's different and not interchangeable. Let's talk about the difference.

A truthy value

NotImplemented is intended for binary special methods to indicate that an operation is not implemented between two types. For example, the comparison 4 < 7 evaluates to True because two integers can be compared with each other, but 4 < Foo(7) won't work. By default, it'll raise a TypeError because '<' not supported between instances of 'int' and 'Foo'.

If you want to override this default behaviour but don't want to implement the comparison operation for all possible types out there, you can use NotImplemented. It would look like this:

class Foo:
    def __init__(self, x):
        self.x = x
    
    def __lt__(self, other):
        if isinstance(other, Foo):
            return self.x < other.x
        else:
            return NotImplemented

This can be convenient for binary special methods that are commonly used (e.g. in list comprehensions or in sorting functions) or in cases where it's awkward or undesirable to have an exception halt the program.

That said, remember that bool(NotImplemented) is always truthy! If you forget, Python now comes with a warning when the boolean evaluation is used: DeprecationWarning: NotImplemented should not be used in a boolean context.

So what about NotImplementedError?

An exceptional situation

NotImplementedError is a runtime error used by abstract classes that require their children to override a method. It's also used during development as a TODO placeholder that fails loudly if you forget to implement a method before going to production.

It should not be used with binary special methods. Leave that for NotImplemented.

Ellipsis

Yet another singleton! Unlike the other built-in constants, it doesn't have a predefined purpose, so it tends to pop up in code everywhere.

In Python 3

Sometimes, it's a placeholder for a function not yet implemented:

def foo():
    ...

Other times, it's a placeholder for partial type hints:

Callable[..., int]
Tuple[int, ...]

In libraries

FastAPI uses it to specify required parameters:

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: str = Query(..., min_length=3)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

And numpy uses it to slice multi-dimensional arrays:

>>> foo = numpy.arange(16).reshape(2, 2, 2, 2)

>>> foo
array([[[[ 0,  1], [ 2,  3]],
        [[ 4,  5], [ 6,  7]]],
       [[[ 8,  9], [10, 11]],
        [[12, 13], [14, 15]]]])

>>> foo[1,...,1]  # same as foo[1,:,:,1]
array([[ 9, 11],
       [13, 15]])

A versatile constant, and you can use it in either form: ... or Ellipsis. Fancy!

Exploring the architecture of Birmingham Bullring Shopping Center from different perspectives.

__debug__

This is the last of the built-in constants. The __debug__ flag is rarely used, and there's not much to say. It's just a boolean variable that informs what debug mode Python is running in, and it's set to True by default. Usually, it's used to insert some extra print or log statements, but not much more:

if __debug__:
	print("I'm just a print statement")

Similar to other built-in constants, its value cannot be manipulated directly. To unset it, either run Python with the -O flag or set the environment variable PYTHONOPTIMIZE=1.

Conclusion

Whew! That was a lot, and yet there were only six. Hopefully, you learned something interesting and will improve how you use the built-in constants in your code.