Learnixo
Back to blog
AI Systemsintermediate

What is the difference between == and is?

Understand Python's equality operator (==) vs identity operator (is): when to use each, common bugs with None checks, integer caching, and string interning.

Asma Hafeez KhanMay 16, 20265 min read
PythonEqualityIdentityNoneis==
Share:š•

The Core Difference

  • == compares values (calls __eq__)
  • is compares identity — whether two variables point to the exact same object in memory
Python
a = [1, 2, 3]
b = [1, 2, 3]   # Same values, different objects
c = a           # Same object

print(a == b)   # True  — same values
print(a is b)   # False — different objects in memory
print(a is c)   # True  — c is the same object as a

# Verify with id()
print(id(a))   # e.g., 140234567890
print(id(b))   # Different number
print(id(c))   # Same number as id(a)

is for None Checks

The most important use of is in Python: always check for None with is, not ==:

Python
result = None

# Correct
if result is None:
    print("No result yet")

if result is not None:
    process(result)

# Works but not idiomatic — PEP 8 recommends `is None`
if result == None:
    print("No result yet")


# Why `is`? Because a custom class can override __eq__ to make == None return True
class ConfusingClass:
    def __eq__(self, other):
        return True   # Equal to everything, including None!

obj = ConfusingClass()
print(obj == None)   # True — unexpected
print(obj is None)   # False — correct: obj is not None

is for Boolean Checks

Similarly, use is to check True and False identity explicitly (though usually just if flag: is preferred):

Python
flag = True

# Idiomatic — check truthiness
if flag:
    print("Flag is truthy")

# Identity check — stricter
if flag is True:
    print("Flag is exactly True, not just truthy")

# Difference: 1 == True but 1 is not True
print(1 == True)   # True  — int 1 equals bool True (True is a subclass of int)
print(1 is True)   # False — different objects

print(0 == False)  # True
print(0 is False)  # False

Integer Caching (CPython Implementation Detail)

CPython caches small integers (-5 to 256) — they're always the same object:

Python
a = 100
b = 100
print(a is b)   # True — cached, same object

a = 1000
b = 1000
print(a is b)   # False — large ints are different objects
# (May be True in some contexts due to compiler optimizations)

Do not rely on integer caching — it's an implementation detail of CPython, not a language guarantee. Never use is to compare integers.


String Interning

Python automatically interns short strings that look like identifiers (no spaces, no special characters). Interned strings share the same object:

Python
a = "warfarin"
b = "warfarin"
print(a is b)   # True — interned (simple identifier-like string)

a = "warfarin aspirin interaction"
b = "warfarin aspirin interaction"
print(a is b)   # Probably False — not interned (spaces present)

# Force interning with sys.intern
import sys
a = sys.intern("warfarin aspirin interaction")
b = sys.intern("warfarin aspirin interaction")
print(a is b)   # True — now interned

Never use is to compare strings — interning behavior is an implementation detail, not a contract.


== and Custom Classes

== calls __eq__. You can define what equality means for your class:

Python
from dataclasses import dataclass

@dataclass
class Drug:
    name: str
    dose_mg: float

# @dataclass automatically generates __eq__ based on fields
warfarin_a = Drug("warfarin", 5.0)
warfarin_b = Drug("warfarin", 5.0)
warfarin_c = Drug("warfarin", 10.0)

print(warfarin_a == warfarin_b)   # True  — same name and dose
print(warfarin_a == warfarin_c)   # False — different dose
print(warfarin_a is warfarin_b)   # False — different objects


# Custom __eq__ without @dataclass
class DrugManual:
    def __init__(self, name: str, dose_mg: float):
        self.name = name
        self.dose_mg = dose_mg

    def __eq__(self, other):
        if not isinstance(other, DrugManual):
            return NotImplemented
        return self.name.lower() == other.name.lower() and self.dose_mg == other.dose_mg

    def __hash__(self):
        return hash((self.name.lower(), self.dose_mg))
        # If you define __eq__, you must define __hash__ if you want the class to be hashable

Common Bugs

Python
# Bug 1: Using == None instead of is None
# Fragile — breaks with classes that override __eq__
if some_result == None:   # Use is None instead
    ...

# Bug 2: Using is for value comparison
x = 1000
y = 1000
if x is y:   # WRONG — False for large ints even if equal
    print("Equal")  # Won't print despite x == y being True

# Use == for value comparison
if x == y:   # Correct
    print("Equal")

# Bug 3: Checking mutable container emptiness with is
my_list = []
if my_list is None:    # WRONG — empty list is not None
    print("No data")

if not my_list:        # Correct — empty list is falsy
    print("No data")

if my_list is not None and len(my_list) == 0:  # Also correct, more explicit
    print("No data")

When to Use Each

| Situation | Use | |---|---| | Compare values (numbers, strings, lists) | == | | Check for None | is None / is not None | | Check for True / False (explicitly) | is True / is False | | Check if two variables point to same object | is | | Compare custom objects | == (define __eq__) | | Compare strings | == (never is) | | Compare integers | == (never is) |


Quick Reference

Python
# Always correct
result is None
result is not None
value == 42
text == "warfarin"
list_a == list_b

# Correct for identity checks only
a is b           # Same object check
obj is True      # Exactly True (not just truthy)

# Never do these
value is 42              # Unreliable for ints > 256
text is "warfarin"       # Unreliable for strings
result == None           # Use is None instead

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.