В Python есть шесть предопределённых «констант»: True, False, None, __debug__, Ellipsis (или, что то же самое, ...) и NotImplemented. Однако по какой-то причине все они ведут себя немного по-разному.

True, False и None

True, False и None — это ключевые слова. Они не являются идентификаторами, а представляют собой отдельные лексические токены. Это довольно необычно: больше ничего подобного в Python нет. Обычно всё разрешается на этапе обычного поиска имён, а не прямо в лексере.

Интересное следствие этого — выражения вида x.True вызывают SyntaxError. Остаётся только гадать, чем было продиктовано такое решение (если оно вообще было осознанным).

С этими константами связаны и другие любопытные моменты, но о них — чуть позже, поскольку они пересекаются с остальными константами.

__debug__

__debug__ — булева константа: обычно она равна True, но при запуске с флагом -O становится False. Идея похожа на то, как отключается assert в «недебажных» сборках: можно обернуть код в if __debug__, если проверка была бы слишком затратной в «оптимизированной» сборке.

__debug__ интересен тем, что, хотя формально это обычный идентификатор (в отличие от True, False и None), это единственный идентификатор в языке, которому нельзя присвоить значение:

>>> __debug__ = 67
  File "<stdin>", line 1
SyntaxError: cannot assign to __debug__

Присвоить значение нельзя даже как атрибуту:

>>> x.__debug__ = 67
  File "<stdin>", line 1
SyntaxError: cannot assign to __debug__

Опять же, ни один другой идентификатор так себя не ведёт. Это по-настоящему особый случай.

Но поскольку это не ключевое слово, ведёт он себя чуть иначе, чем True, False и None:
x.__debug__ вызывает AttributeError (а не SyntaxError), поскольку синтаксически конструкция корректна — просто идёт обращение к несуществующему атрибуту.

Любопытно, что существует и специальное сообщение об ошибке при попытке удалить __debug__ (хотя без этого особого случая всё равно возникла бы NameError), но это не распространяется на удаление атрибута с именем __debug__:

>>> del __debug__
  File "<stdin>", line 1
SyntaxError: cannot delete __debug__
>>> del x.__debug__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined

Если бы x было определено, возникла бы AttributeError. В любом случае это не SyntaxError (в отличие от присваивания) — почему-то.

Отступление: SyntaxError — не всегда правда

Раз уж речь зашла об ошибках: присваивание значения __debug__ — один из немногих известных случаев, когда SyntaxError возникает, хотя формально синтаксис вполне корректен. В этом легко убедиться самому:

>>> assert (__debug__ := 67)

Выполнение этого assert в debug-сборке вызовет SyntaxError, но с флагом -O assert вообще не компилируется, поэтому никакого исключения не возникнет.

Ещё два похожих случая — использование yield или await вне функции:

>>> assert (yield)
>>> assert (await 67)

Ellipsis и NotImplemented

Ellipsis и NotImplemented описаны в разделе «константы» справочника, но, в отличие от остальных четырёх, «настоящими» константами не являются. Это обычные встроенные объекты, поэтому их можно затенить глобальными переменными:

>>> NotImplemented = 67
>>> NotImplemented
67

И снова возникает вопрос: почему эти два не особые, а остальные константы — особые?

Перезапись констант

Вот ещё один интересный момент: несмотря на то, что True, False и None — лексические токены, они одновременно существуют и как обычные встроенные объекты:

>>> import builtins
>>> getattr(builtins, 'True')
True
>>> getattr(builtins, 'False')
False
>>> getattr(builtins, 'None') is None
True

Обратиться к ним напрямую, минуя getattr, не получится.

А вот тут начинается самое интересное: setattr тоже работает!

>>> setattr(builtins, 'True', 67)
>>> getattr(builtins, 'True')
67

Однако это не меняет значение при обращении через лексический токен:

>>> True
True

Но __debug__ ведёт себя точно так же!

>>> setattr(builtins, '__debug__', 67)
>>> builtins.__debug__
67
>>> __debug__
True

Получается, значение __debug__ в каком-то смысле можно изменить, но, несмотря на то, что это не лексический токен, для него сделано такое же исключение, как для True, False и None: его значение не зависит от изменений модуля builtins. Так что это действительно константа!

Ellipsis и NotImplemented, повторимся, настоящими константами не являются:

>>> setattr(builtins, 'Ellipsis', 67)
>>> Ellipsis
67

Но при этом значение ... не меняется:

>>> ...
Ellipsis

Получается, в каком-то смысле ... — настоящая константа, а вот Ellipsis — нет. Странно, не правда ли?