Короткий ответ
Кеширование маленьких целых чисел (-5..256) и интернирование некоторых строковых литералов — это деталь реализации конкретной версии CPython, а не гарантия языка Python, поэтому код, зависящий от is для сравнения значений, может работать случайно на одной версии/сборке интерпретатора и давать иной результат на другой, в другом окружении выполнения или при значениях, пришедших не как литералы (например, построенных конкатенацией в runtime); правильная практика — всегда использовать == для сравнения значений и оставлять is только для сравнения идентичности объектов и синглтонов (None, True, False).
Как это работает подробнее
CPython по соображениям производительности кеширует небольшие целые числа и интернирует некоторые строки (в основном короткие идентификатор-подобные литералы, определяемые во время компиляции), поэтому два отдельных литерала с одинаковым маленьким значением иногда действительно оказываются одним и тем же объектом в памяти, и is между ними возвращает True. Но это оптимизация реализации, не являющаяся частью языковой спецификации: она не гарантирована для строк, полученных во время выполнения (например, через str(x), конкатенацию, .format(), чтение из файла/сети/БД), и может отличаться между версиями CPython или вообще не выполняться в альтернативных реализациях (PyPy и другие).
Пример кода
a = "pending"
b = "pending"
print(a is b) # True на CPython для таких литералов — деталь реализации
c = "pend" + "ing" # собрано в runtime из частей
print(a is c) # не гарантировано True, часто False
status_from_db = str("pending") # смоделируем значение, пришедшее не как литерал
print(a == status_from_db) # True — корректно работает всегда
print(a is status_from_db) # ненадёжно, не гарантировано
SENTINEL = object() # для настоящих синглтонов приложения is — правильный инструмент
def get(value=SENTINEL):
return value is SENTINELЧто использовать на практике
- Практика. На код-ревью такой паттерн стоит трактовать как потенциальный баг, а не как "преждевременную оптимизацию, которая просто странно выглядит": строка статуса, прочитанная из БД, десериализованная из JSON или полученная из внешнего API, почти никогда не будет тем же самым интернированным объектом, что строковый литерал в коде, даже если их текстовое значение совпадает —
isв этом случае может молча вернутьFalseтам, где==вернул бы ожидаемыйTrue, и приложение примет неверную бизнес-логику без явной ошибки, что особенно опасно, так как баг проявляется не всегда, а в зависимости от того, как именно строка была создана.
Подводные камни
- Риски. Пишут код, который "работает на машине разработчика" именно из-за совпадения деталей реализации интернирования для конкретных литералов, и не замечают проблему до тех пор, пока то же сравнение не столкнётся со строкой иного происхождения (из БД, после сериализации) в продакшене. Также путают
isдляNone/True/False(где это действительно правильная и идиоматичная практика, так как это настоящие синглтоны, гарантированные языком) сisдля произвольных строк/чисел, где такой гарантии нет. - Компромисс. Строгого компромисса здесь по сути нет — для сравнения значений правильный инструмент почти всегда
==, и отступать от этого правила в пользуisоправдано только в узких, явно документированных случаях работы с действительно синглтон-подобными объектами на уровне самого приложения (например, собственный sentinel-объект_UNSET = object(), созданный и используемый ровно как один объект). Обсуждение на ревью должно смещаться не к "может сработать", а к тому, что корректность кода не должна зависеть от деталей реализации интерпретатора, которые явно не документированы как гарантия.