CPython Internals: How the 'is' and 'is not' Operators Are Implemented

Arpit Bhayani

Arpit Bhayani

May 31, 2021 • 6 min read

Play

In Python, the is operator tests for object identity. Rather than evaluating value equivalence (such as the == operator, which delegates to __eq__), is evaluates whether two identifiers point to the exact same object residing at the same memory address.

While this concept seems deceptively simple on the surface, peeling back the layers of CPython reveals profound software engineering decisions—spanning PEG grammars, AST compilation, virtual machine bytecode, and a branchless bitwise optimization in the core C execution loop.


1. Identity vs. Value Equivalence in Python

In CPython, every object has an identity representing its actual memory address (obtainable via id(obj)). When executing:

a = 10
b = 10
a is b  # Returns True due to small integer caching [-5, 256]

x = "arpit"
y = "arpit"
x is y  # Returns True due to string interning

Under the hood, a is b evaluates whether id(a) == id(b), or in C pointer semantics: whether pointer a is equal to pointer b.


2. Inspecting Bytecode via dis

To understand how Python evaluates identity, we can disassemble code using the standard library dis module:

import dis

dis.dis("a is b")

Output:

  1           0 LOAD_NAME                0 (a)
              2 LOAD_NAME                1 (b)
              4 IS_OP                    0
              6 RETURN_VALUE

Now disassemble the inverse check:

dis.dis("a is not b")

Output:

  1           0 LOAD_NAME                0 (a)
              2 LOAD_NAME                1 (b)
              4 IS_OP                    1
              6 RETURN_VALUE

Notice that both operations map directly to the exact same bytecode instruction: IS_OP. The only difference is the opcode argument (oparg):

  • For a is b, oparg is 0.
  • For a is not b, oparg is 1.

3. The Execution Loop: ceval.c

In CPython, all bytecode instructions are evaluated in the evaluation loop inside Python/ceval.c. Around line 3611 (in Python 3.10), the handler for IS_OP is defined.

TARGET(IS_OP) {
    PyObject *right = POP();
    PyObject *left = TOP();
    int res = (left == right) ^ oparg;
    PyObject *b = res ? Py_True : Py_False;
    Py_INCREF(b);
    SET_TOP(b);
    Py_DECREF(left);
    Py_DECREF(right);
    PREDICT(POP_JUMP_IF_FALSE);
    PREDICT(POP_JUMP_IF_TRUE);
    DISPATCH();
}

Stack Manipulation

  1. PyObject *right = POP();: Pops the top operand off the evaluation stack (which is b).
  2. PyObject *left = TOP();: Peeks at the next element on top of the stack (which is a) without executing an unnecessary pop.
  3. SET_TOP(b);: Once evaluated, overwrites the current stack top directly with the boolean result (Py_True or Py_False). This avoids an extra pop-and-push sequence, minimizing stack pointer churn.
  4. Py_DECREF(left) and Py_DECREF(right): Decrements reference counts for the operands since their evaluation context has completed.

4. The Branchless XOR Trick

Look closely at how res is calculated:

int res = (left == right) ^ oparg;

In C, left == right evaluates whether the two memory addresses (pointers) are identical. If they point to the exact same struct in memory, left == right yields 1; otherwise, 0.

Instead of writing conditional branching logic:

// A conventional but slower approach
if (oparg == 1) {
    res = !(left == right);
} else {
    res = (left == right);
}

CPython exploits the truth table of the bitwise XOR (^) operator:

Expression (left == right)opargOperation (expr ^ oparg)Final resMeaning
1 (Same Object)0 (is)1 ^ 01True
0 (Different Objects)0 (is)0 ^ 00False
1 (Same Object)1 (is not)1 ^ 10False
0 (Different Objects)1 (is not)0 ^ 11True

Why Use XOR Instead of an if/else Branch?

  1. Branch Predictor Penalties: Modern CPUs execute instructions via deep instruction pipelines. A conditional branch (if/else) introduces the risk of branch misprediction, causing pipeline flushes that cost 10–20 CPU cycles.
  2. Single-Cycle Native ALU Operation: Bitwise XOR maps to a single CPU instruction (e.g., xor on x86/ARM) that executes deterministically in a single clock cycle without jumping.

5. From Python Source to Bytecode

How do is and is not get translated into IS_OP 0 and IS_OP 1? The pipeline spans the PEG parser grammar and compiler.

1. Grammar Definition (Grammar/python.gram)

In Python 3.9+, CPython uses a PEG (Parsing Expression Grammar) parser. The comparison rules identify is and is not:

comparison:
    | ...
    | 'is' 'not' { _PyPegen_cmpop_expr_pair(p, PyCmp_IS_NOT, a) }
    | 'is' { _PyPegen_cmpop_expr_pair(p, PyCmp_IS, a) }

2. Bytecode Generation (Python/compile.c)

During abstract syntax tree (AST) compilation, Python/compile.c maps comparison operations to virtual machine opcodes:

case PyCmp_IS:
    ADDOP_I(c, IS_OP, 0);
    break;
case PyCmp_IS_NOT:
    ADDOP_I(c, IS_OP, 1);
    break;

Here, ADDOP_I emits the opcode (IS_OP) along with its integer argument (oparg).


6. Contrast: Why Not Use COMPARE_OP?

General comparison operators (<, <=, ==, !=, >, >=) compile into the COMPARE_OP instruction rather than specialized single-purpose opcodes.

For example, disassembling a greater-than-or-equal check:

dis.dis("a >= b")

Output:

  1           0 LOAD_NAME                0 (a)
              2 LOAD_NAME                1 (b)
              4 COMPARE_OP               5 (>=)
              6 RETURN_VALUE

In compile.c, general comparisons map to predefined integer constants:

  • Py_LT = 0 (<)
  • Py_LE = 1 (<=)
  • Py_EQ = 2 (==)
  • Py_NE = 3 (!=)
  • Py_GT = 4 (>)
  • Py_GE = 5 (>=)

These general operators require dynamic method resolution at runtime (e.g., calling tp_richcompare or user-defined __ge__ methods). In contrast, identity operations cannot be overridden by user classes. By granting identity checks their own dedicated opcode (IS_OP), CPython bypasses rich comparison lookups and resolves identity in a single branchless pointer evaluation.


Summary of Key Takeaways

  • Identity is Pointer Equality: Under the hood, a is b in CPython is simply checking whether the C pointers left == right.
  • Unified Instruction: Both is and is not share the exact same opcode (IS_OP), distinguished only by an argument 0 or 1.
  • Branchless Programming: The expression (left == right) ^ oparg handles both is and is not in a single CPU cycle without incurring branching overhead or pipeline stalls.
  • Stack Optimization: CPython replaces the top of the stack (SET_TOP) directly rather than performing redundant pops and pushes.
Arpit Bhayani

Principal Engineer II at Razorpay - building Agent Studio, Ex-staff engg at GCP Memorystore & Dataproc, Creator of DiceDB, ex-Amazon Fast Data, ex-Director of Engg. SRE and Data Engineering at Unacademy. I spark engineering curiosity through my no-fluff engineering videos on YouTube and my courses