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
PyObject *right = POP();: Pops the top operand off the evaluation stack (which is b).
PyObject *left = TOP();: Peeks at the next element on top of the stack (which is a) without executing an unnecessary pop.
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.
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) | oparg | Operation (expr ^ oparg) | Final res | Meaning |
|---|
1 (Same Object) | 0 (is) | 1 ^ 0 | 1 | True |
0 (Different Objects) | 0 (is) | 0 ^ 0 | 0 | False |
1 (Same Object) | 1 (is not) | 1 ^ 1 | 0 | False |
0 (Different Objects) | 1 (is not) | 0 ^ 1 | 1 | True |
Why Use XOR Instead of an if/else Branch?
- 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.
- 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.