Python Identity Operators

Python identity operators are a concept that trips up many beginners, especially those coming from other programming languages. When you compare two variables in Python, you have two very different questions you can ask: are these values equal, or are these the exact same object in memory? Python identity operators answer that second question. The is and is not operators check whether two variables refer to the exact same object, not just whether they hold the same value — and understanding that distinction is essential to writing correct Python.

What Are Python Identity Operators

Python provides two identity operators:

  • is — returns True if both variables point to the same object in memory
  • is not — returns True if both variables point to different objects in memory

These are different from equality operators (== and !=). The equality operator checks whether two values are the same. The Python identity operator checks whether two variables are literally the same object — stored at the same memory address. Think of it like asking whether two people have the same name versus whether you are literally looking at the same person.

Here is a quick first look at Python identity operators in action:

python
a = [1, 2, 3]
b = a
c = [1, 2, 3]

print(a is b)
print(a is c)
print(a == c)
Output
True
False
True

Here b was assigned a directly, so both variables point to the same list object in memory — a is b returns True. The variable c holds a list with identical contents, but it is a completely separate object — so a is c returns False even though a == c is True.

How Python Identity Works Under the Hood

To really understand Python identity operators, you need to understand how Python manages objects in memory. Every object in Python has a unique identity — a numeric ID that represents its memory address. You can inspect this using Python's built-in id() function.

python
x = "hello"
y = "hello"
z = x

print(id(x))
print(id(y))
print(id(z))

print(x is y)
print(x is z)
Output
140234567890112
140234567890112
140234567890112
True
True

You might notice that x is y returns True here even though x and y were assigned separately. That is because Python applies an optimization called string interning for short strings and string literals. Python caches certain immutable objects and reuses them rather than creating duplicates. So x and y end up pointing to the same string object in memory.

This is exactly why you should not rely on is to compare strings in general — the behavior depends on Python's internal optimizations, which are not guaranteed. Use == to compare values and is only when you genuinely need to check object identity.

python
a = "hello world this is a longer string"
b = "hello world this is a longer string"

print(a == b)
print(a is b)
Output
True
False

For longer strings that Python does not intern, the two variables are separate objects in memory, so is returns False even though the values are identical.

The is Operator in Python with None

The single most common and correct use of the is operator in Python is checking for None. Python guarantees that there is exactly one None object in the entire runtime. Every variable set to None points to that same singleton object. Because of this guarantee, the Python community convention is to always use is (not ==) when checking for None.

python
def find_user(user_id):
    if user_id == 0:
        return None
    return {"id": user_id, "name": "Alice"}

result = find_user(0)

if result is None:
    print("No user found")
else:
    print("User found:", result)
Output
No user found

Using result is None is both semantically correct and idiomatic Python. It directly expresses the intent: check if result is the None singleton, not just whether it equals some value that happens to be falsy.

You will often see the is not operator used alongside None checks in the same way:

python
config = {"debug": True}

if config is not None:
    print("Config loaded:", config)
Output
Config loaded: {'debug': True}

This pattern is extremely common in Python functions that may or may not return a meaningful value — functions that return None as a sentinel to indicate absence or failure.

Python Identity Operators with Integers

Python also applies object caching to small integers, typically in the range of -5 to 256. These integers are pre-allocated when the Python interpreter starts and reused whenever you create a variable with that value. This is called the integer cache or small integer cache.

python
a = 100
b = 100

print(a is b)
print(id(a), id(b))

x = 1000
y = 1000

print(x is y)
print(id(x), id(y))
Output
True
140234567890944 140234567890944
False
140234601234560 140234604567200

The integers 100 and 100 resolve to the same cached object, so a is b returns True. But 1000 and 1000 are outside the cached range — Python creates two separate integer objects, so x is y returns False. This behavior is an implementation detail of CPython and not something you should depend on in production code. It illustrates why Python identity operators are intended for object identity checks, not value comparisons.

The is not Operator in Python

The is not operator is the logical inverse of is. It returns True when two variables point to different objects in memory. It is more readable than writing not (a is b) and is the preferred style in Python.

python
items = [10, 20, 30]
copy = items[:]

print(items is not copy)
print(items == copy)

reference = items
print(items is not reference)
Output
True
True
False

Here copy was created using a slice, which produces a new list object — even though the contents are identical. So items is not copy returns True. The variable reference is just another name pointing to the same list, so items is not reference returns False.

Python Identity Operators vs Equality Operators

Understanding when to use is versus == is one of the most important distinctions in Python. Here is a clear breakdown:

The equality operator == calls the __eq__ method on the object. This means the object gets to define what "equal" means. Two different objects can be equal. The Python identity operator is bypasses any method calls and directly compares memory addresses using id(). No custom behavior is possible — it is purely about object identity.

python
class Box:
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        return self.value == other.value

box1 = Box(42)
box2 = Box(42)

print(box1 == box2)
print(box1 is box2)
Output
True
False

The two Box objects have the same value, so == returns True because __eq__ says so. But they are two separate objects in memory, so is returns False. This is a perfect illustration of the difference: equality is about value semantics, identity is about object semantics.

Checking Object Identity with id() and is Together

A useful debugging technique when learning Python identity operators is to use id() alongside is to verify what is happening in memory. When a is b is True, it is always true that id(a) == id(b) — they are two ways of expressing the same fact.

python
def show_identity(name_a, obj_a, name_b, obj_b):
    print(f"{name_a} id: {id(obj_a)}")
    print(f"{name_b} id: {id(obj_b)}")
    print(f"{name_a} is {name_b}: {obj_a is obj_b}")
    print()

nums = [1, 2, 3]
alias = nums
clone = list(nums)

show_identity("nums", nums, "alias", alias)
show_identity("nums", nums, "clone", clone)
Output
nums id: 140234567892416
alias id: 140234567892416
nums is alias: True

nums id: 140234567892416
clone id: 140234601235584
nums is clone: False

The alias shares the same memory address as nums, confirming they are the same object. The clone has a different address because list() creates a new object with copied contents.

When to Use Python Identity Operators

There are clear guidelines in Python for when identity operators are the right choice:

Use is and is not when:

  • Comparing with None — this is the most common and idiomatic use
  • Checking if two variables refer to the same mutable object (like confirming an alias)
  • Working with singletons or sentinel objects you have created yourself

Use == and != when:

  • Comparing values regardless of whether they are the same object
  • Comparing strings, numbers, lists, dicts, or any data for content equality
  • Working with custom classes where equality has been defined

The Python style guide PEP 8 explicitly recommends using is and is not for None comparisons: "Comparisons to singletons like None should always be done with is or is not, never the equality operators."

Full Working Example

This example brings together all aspects of Python identity operators — comparing with None, checking object references, using id(), and understanding the difference from equality operators.

python
class Config:
    def __init__(self, settings):
        self.settings = settings

    def __eq__(self, other):
        if not isinstance(other, Config):
            return False
        return self.settings == other.settings


def load_config(source):
    if source == "empty":
        return None
    return Config({"theme": "dark", "debug": False})


# None identity check
cfg = load_config("empty")
if cfg is None:
    print("No config loaded — using defaults")

cfg = load_config("file")
if cfg is not None:
    print("Config loaded successfully")
    print("Settings:", cfg.settings)

# Identity vs equality with Config objects
cfg1 = Config({"theme": "dark", "debug": False})
cfg2 = Config({"theme": "dark", "debug": False})
cfg3 = cfg1

print("\nIdentity checks:")
print(f"cfg1 is cfg2: {cfg1 is cfg2}")
print(f"cfg1 is cfg3: {cfg1 is cfg3}")

print("\nEquality checks:")
print(f"cfg1 == cfg2: {cfg1 == cfg2}")
print(f"cfg1 == cfg3: {cfg1 == cfg3}")

print("\nMemory addresses:")
print(f"id(cfg1): {id(cfg1)}")
print(f"id(cfg2): {id(cfg2)}")
print(f"id(cfg3): {id(cfg3)}")

# Integer cache behavior
print("\nInteger cache examples:")
small_a = 50
small_b = 50
large_a = 5000
large_b = 5000
print(f"50 is 50: {small_a is small_b}")
print(f"5000 is 5000: {large_a is large_b}")

# None singleton confirmation
x = None
y = None
print(f"\nNone is None: {x is y}")
print(f"id(x): {id(x)}, id(y): {id(y)}")
Output
No config loaded — using defaults
Config loaded successfully
Settings: {'theme': 'dark', 'debug': False}

Identity checks:
cfg1 is cfg2: False
cfg1 is cfg3: True

Equality checks:
cfg1 == cfg2: True
cfg1 == cfg3: True

Memory addresses:
id(cfg1): 140234567893120
id(cfg2): 140234601236992
id(cfg3): 140234567893120

Integer cache examples:
50 is 50: True
5000 is 5000: False

None is None: True
id(x): 9456832, id(y): 9456832

This example shows every key behavior of Python identity operators in one place: the None singleton check, the difference between identity and equality when __eq__ is defined, how memory addresses confirm what is tells you, and how the integer cache affects identity for small versus large numbers.