Comparison of Python's `is` and `==` Operators

This article was written in September 2020. It is a short report on some interesting parts of the CPython interpreter, written for the “Computer Architecture Theory 2” course at my university. The writing may be a bit messy. If you are interested, please read on. :)

Summary

This report mainly uses source-code reading and code experiments to understand CPython’s internal implementation of comparison operators and identity operators.

I found that comparison operators such as == and < are processed as COMPARE_OP bytecode. CPython tries the tp_richcompare slots of the operand types according to a defined priority; for user-defined classes, these slots invoke special methods such as __eq__. If neither operand provides a result, == and != fall back to an identity comparison. For ease of use, I have organized a list of the built-in types’ richcmp methods. I will provide more details and describe the analysis process in the article.

Finally, I provide a simple analysis of the caching mechanism for small numbers in CPython’s int type. I found that CPython caches integer objects in the range [-5, 256]. The analysis appears in the section <Other interesting findings>.

Introduction

Python has a pair of identity operators: is and is not.

They can be used to determine whether two references point to the same object. In CPython, this is implemented by comparing object pointers, as shown below:

1
2
3
4
5
6
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a == b
True
>>> a is b
False

However, the following behavior may seem unintuitive and confusing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
>>> a = 1
>>> b = 1
>>> a is b
True
>>> a == b
True
>>> a = 114514
>>> b = 114514
>>> a is b
False
>>> a == b
True

Since identity operators do not exist in many other languages, many beginners easily confuse is with the == operator. Many learning materials do not explain the distinction thoroughly, so I wanted to study the implementation principles.

This analysis is based on the latest CPython 3.10 development version from the master branch as of August 20, 2020. I found that its implementation differs considerably from the stable CPython 3.8 release available at the time, so please pay attention to the version difference. Now let’s move on to the main topic.

Analysis process

First, let’s pull and compile the latest source code. In the Python shell, we can use the dis module to inspect the CPython bytecode for is and ==:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> import dis
>>> def test():
...     a = 1
...     b = 1
...     a is b
...     a == b
...
>>> dis.dis(test)
  2           0 LOAD_CONST               1 (1)
              2 STORE_FAST               0 (a)

  3           4 LOAD_CONST               1 (1)
              6 STORE_FAST               1 (b)

  4           8 LOAD_FAST                0 (a)
             10 LOAD_FAST                1 (b)
             12 IS_OP                    0
             14 POP_TOP

  5          16 LOAD_FAST                0 (a)
             18 LOAD_FAST                1 (b)
             20 COMPARE_OP               2 (==)
             22 POP_TOP
             24 LOAD_CONST               0 (None)
             26 RETURN_VALUE

As we can see from the bytecode, in the latest 3.10 development version, is uses the IS_OP execution path (oparg = 0), while == uses the COMPARE_OP execution path (oparg = 2).

is operator

Following this clue, I searched the source code for IS_OP and found the following code in Python/ceval.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
case 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);
    FAST_DISPATCH();
}

We can see that the core logic of the IS_OP operator is very simple. In CPython, all Python objects are represented by PyObject, so the left operand, left, and the right operand, right, are pointers. The is operator compares these two pointers. If they are equal, it returns Py_True; otherwise, it returns Py_False.

== operator

What about the == operator? I searched the source code for COMPARE_OP and found the following code in Python/ceval.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
case TARGET(COMPARE_OP): {
    assert(oparg <= Py_GE);
    PyObject *right = POP();
    PyObject *left = TOP();
    PyObject *res = PyObject_RichCompare(left, right, oparg);
    SET_TOP(res);
    Py_DECREF(left);
    Py_DECREF(right);
    if (res == NULL)
        goto error;
    PREDICT(POP_JUMP_IF_FALSE);
    PREDICT(POP_JUMP_IF_TRUE);
    DISPATCH();
}

The code calls PyObject_RichCompare, passing the left operand as left, the right operand as right, and the rich-comparison opcode (for example, Py_EQ) as oparg. Let’s examine the source code of this function, which is located in Objects/object.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Perform a rich comparison with object result.  This wraps do_richcompare()
   with a check for NULL arguments and a recursion check. */

PyObject *
PyObject_RichCompare(PyObject *v, PyObject *w, int op)
{
    PyThreadState *tstate = _PyThreadState_GET();

    assert(Py_LT <= op && op <= Py_GE);
    if (v == NULL || w == NULL) {
        if (!_PyErr_Occurred(tstate)) {
            PyErr_BadInternalCall();
        }
        return NULL;
    }
    if (_Py_EnterRecursiveCall(tstate, " in comparison")) {
        return NULL;
    }
    PyObject *res = do_richcompare(tstate, v, w, op);
    _Py_LeaveRecursiveCall(tstate);
    return res;
}

The main body of this function performs safety checks. The most important step is the call to do_richcompare, so let’s continue:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/* Perform a rich comparison, raising TypeError when the requested comparison
   operator is not supported. */
static PyObject *
do_richcompare(PyThreadState *tstate, PyObject *v, PyObject *w, int op)
{
    richcmpfunc f;
    PyObject *res;
    int checked_reverse_op = 0;

    if (!Py_IS_TYPE(v, Py_TYPE(w)) &&
        PyType_IsSubtype(Py_TYPE(w), Py_TYPE(v)) &&
        (f = Py_TYPE(w)->tp_richcompare) != NULL) { // <- 1st case
        checked_reverse_op = 1;
        res = (*f)(w, v, _Py_SwappedOp[op]);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    if ((f = Py_TYPE(v)->tp_richcompare) != NULL) { // <- 2nd case
        res = (*f)(v, w, op);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    if (!checked_reverse_op && (f = Py_TYPE(w)->tp_richcompare) != NULL) { // <- 3rd case
        res = (*f)(w, v, _Py_SwappedOp[op]);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    /* If neither object implements it, provide a sensible default
       for == and !=, but raise an exception for ordering. */
    switch (op) { // <- other situations
    case Py_EQ:
        res = (v == w) ? Py_True : Py_False;
        break;
    case Py_NE:
        res = (v != w) ? Py_True : Py_False;
        break;
    default:
        _PyErr_Format(tstate, PyExc_TypeError,
                      "'%s' not supported between instances of '%.100s' and '%.100s'",
                      opstrings[op],
                      Py_TYPE(v)->tp_name,
                      Py_TYPE(w)->tp_name);
        return NULL;
    }
    Py_INCREF(res);
    return res;
}

This function is relatively long, so let’s analyze it part by part.

Rich comparison

This function uses tp_richcompare and the constants Py_EQ and Py_NE. We can find the relevant code in Include/object.h:

1
2
3
4
5
6
7
/* Rich comparison opcodes */
#define Py_LT 0
#define Py_LE 1
#define Py_EQ 2
#define Py_NE 3
#define Py_GT 4
#define Py_GE 5

CPython types expose behavior through slots in PyTypeObject; this mechanism predates Python 3.8. By searching the Python C API documentation, we can find the special methods that correspond to tp_richcompare.

PyTypeObject SlotTypespecial methods/attrsInfo
tp_richcomparerichcmpfunc__lt__, __le__, __eq__, __ne__, __gt__, __ge__XG

Now look at the documentation for tp_richcompare:

1
PyObject *tp_richcompare(PyObject *self, PyObject *other, int op);

The first parameter is guaranteed to be an instance of the type that is defined by PyTypeObject.

The function should return the result of the comparison (usually Py_True or Py_False). If the comparison is undefined, it must return Py_NotImplemented, if another error occurred it must return NULL and set an exception condition.

I organized this information into the following table:

OperatorSpecial methodOpcode
<__lt__Py_LT = 0
<=__le__Py_LE = 1
==__eq__Py_EQ = 2
!=__ne__Py_NE = 3
>__gt__Py_GT = 4
>=__ge__Py_GE = 5

We can override any one or more of the above methods to define the behavior of the corresponding operators.

Each object in Python is associated with a type. The type contains a tp_richcompare function pointer that determines the behavior of rich comparisons between objects.

By calling the tp_richcompare function for a given type, CPython runs the corresponding comparison logic. For user-defined classes, this generally invokes special methods written in Python.

For example, you can define the __eq__ special method in a custom class. The following example shows which method is called by a == comparison:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> class MyClass:
...     def __eq__(self, o):
...         print('__eq__(==) method is called!')
...         return True
...
>>> a = MyClass()
>>> b = MyClass()
>>> a == b
__eq__(==) method is called!
True

The built-in types usually have __eq__ and __ne__. Some built-in types implement all the comparison operations, including __lt__. We can see this by running some simple code:

1
2
3
4
5
6
>>> x = 1
>>> x.__lt__(2)
True
>>> x = {1: 1}
>>> x.__lt__({2: 2})
NotImplemented

Many of these special methods are implemented in C. For example, CPython uses the long_richcompare() function for the int type. I will list several implementations in the Conclusion.

The int expression x < 2 is faster than the explicit call x.__lt__(2), but not because it avoids rich-comparison dispatch. Both ultimately use the int comparison implementation in C; the explicit call also performs attribute lookup and method-call handling. Compare the times below.

1
2
3
4
5
>>> import statistics, timeit
>>> statistics.mean(timeit.repeat("x < 2", setup="x=1", repeat=100, globals=globals()))
0.05961373900000126
>>> statistics.mean(timeit.repeat("x.__lt__(2)", setup="x=1", repeat=100, globals=globals()))
0.11898647200000483

Next, let’s examine the three if statements in the do_richcompare function. They represent three cases.

The first case

1
2
3
4
5
6
7
8
9
if (!Py_IS_TYPE(v, Py_TYPE(w)) &&
    PyType_IsSubtype(Py_TYPE(w), Py_TYPE(v)) &&
    (f = Py_TYPE(w)->tp_richcompare) != NULL) {
    checked_reverse_op = 1;
    res = (*f)(w, v, _Py_SwappedOp[op]);
    if (res != Py_NotImplemented)
        return res;
    Py_DECREF(res);
}

v and w are of different types, and w’s class is a strict subclass of v’s class. If w’s type has a non-NULL tp_richcompare slot, that slot is tried first. If it returns Py_NotImplemented, comparison continues with v. Here is an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> class A:
...     pass
...
>>> class B(A):
...     def __eq__(self, o):
...         print('Do eq richcompare in B')
...         return True
...
>>> a = A()
>>> b = B()
>>> a == b
Do eq richcompare in B
True

The second case

1
2
3
4
5
6
if ((f = Py_TYPE(v)->tp_richcompare) != NULL) {
    res = (*f)(v, w, op);
    if (res != Py_NotImplemented)
        return res;
    Py_DECREF(res);
}

If v’s type has a non-NULL tp_richcompare slot, that slot is tried after the optional subclass-priority check above. If it returns Py_NotImplemented, comparison continues with w. Here is an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> class A:
...     def __eq__(self, o):
...         print('Do eq richcompare in A')
...
>>> class B:
...     pass
...
>>> class C(A):
...     pass
...
>>> class D(B):
...     def __eq__(self, o):
...         print('Do eq richcompare in D')
...
>>> a = A()
>>> b = B()
>>> c = C()
>>> d = D()
>>> a == b
Do eq richcompare in A
>>> a == c
Do eq richcompare in A
>>> a == d
Do eq richcompare in A

The third case

1
2
3
4
5
6
if (!checked_reverse_op && (f = Py_TYPE(w)->tp_richcompare) != NULL) {
    res = (*f)(w, v, _Py_SwappedOp[op]);
    if (res != Py_NotImplemented)
        return res;
    Py_DECREF(res);
}

If w was not already tried as a subclass and v’s comparison returned Py_NotImplemented (or v had no comparison slot), then w’s tp_richcompare slot is tried with the operands and operator swapped. For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> class Left:
...     def __eq__(self, o):
...         print('Do eq richcompare in Left')
...         return NotImplemented
...
>>> class Right:
...     def __eq__(self, o):
...         print('Do eq richcompare in Right')
...         return True
...
>>> Left() == Right()
Do eq richcompare in Left
Do eq richcompare in Right
True

Other situations

Next, the function reaches a switch branch:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* If neither object implements it, provide a sensible default
   for == and !=, but raise an exception for ordering. */
switch (op) {
case Py_EQ:
    res = (v == w) ? Py_True : Py_False;
    break;
case Py_NE:
    res = (v != w) ? Py_True : Py_False;
    break;
default:
    _PyErr_Format(tstate, PyExc_TypeError,
                  "'%s' not supported between instances of '%.100s' and '%.100s'",
                  opstrings[op],
                  Py_TYPE(v)->tp_name,
                  Py_TYPE(w)->tp_name);
    return NULL;
}

If neither operand’s comparison slot returns a result, the function reaches the switch branch. For == and !=, it compares pointers; for an ordering operator, it raises TypeError.

User-defined classes inherit object_richcompare by default, but not every type has the same default slot. The switch branch is reached only when the available slots return Py_NotImplemented, or when the relevant slots are NULL.

We can use the following experiment to verify this behavior.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
>>> class A:
...     pass
...
>>> class B:
...     pass
...
>>> a = A()
>>> b = B()
>>> a == b # the second and third cases return Py_NotImplemented, so the Py_EQ fallback is used
False
>>> a is b # gets the same result as ==
False
>>> a != b # the second and third cases return Py_NotImplemented, so the Py_NE fallback is used
True
>>> a is not b # gets the same result as !=
True
>>> a > b # the second and third cases return Py_NotImplemented, so the default fallback raises TypeError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '>' not supported between instances of 'A' and 'B'

The default implementation of richcompare

Why can we compare instances of class A and class B with == and != even though we have not defined __eq__ or __ne__, while the other comparison operators do not work? I found the relevant code in Objects/typeobject.c:

1
2
3
4
5
PyTypeObject PyBaseObject_Type = {
    ...
    object_richcompare,                         /* tp_richcompare */
    ...
};

So far, we know that instances of user-defined classes inherit the built-in object_richcompare function by default. By looking at this function, we can see that Py_EQ and Py_NE are implemented by default:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
static PyObject *
object_richcompare(PyObject *self, PyObject *other, int op)
{
    PyObject *res;

    switch (op) {

    case Py_EQ:
        /* Return NotImplemented instead of False, so if two
           objects are compared, both get a chance at the
           comparison.  See issue #1393. */
        res = (self == other) ? Py_True : Py_NotImplemented;
        Py_INCREF(res);
        break;

    case Py_NE:
        /* By default, __ne__() delegates to __eq__() and inverts the result,
           unless the latter returns NotImplemented. */
        if (Py_TYPE(self)->tp_richcompare == NULL) {
            res = Py_NotImplemented;
            Py_INCREF(res);
            break;
        }
        res = (*Py_TYPE(self)->tp_richcompare)(self, other, Py_EQ);
        if (res != NULL && res != Py_NotImplemented) {
            int ok = PyObject_IsTrue(res);
            Py_DECREF(res);
            if (ok < 0)
                res = NULL;
            else {
                if (ok)
                    res = Py_False;
                else
                    res = Py_True;
                Py_INCREF(res);
            }
        }
        break;

    default:
        res = Py_NotImplemented;
        Py_INCREF(res);
        break;
    }

    return res;
}

We can see that case Py_EQ performs a simple pointer comparison. It returns Py_True when the pointers are the same and Py_NotImplemented otherwise. If it returns Py_NotImplemented, comparison continues according to the dispatch priority.

Note that in the Py_NE case, the function tries Py_EQ if tp_richcompare has been implemented:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
res = (*Py_TYPE(self)->tp_richcompare)(self, other, Py_EQ);
if (res != NULL && res != Py_NotImplemented) {
    int ok = PyObject_IsTrue(res);
    Py_DECREF(res);
    if (ok < 0)
        res = NULL;
    else {
        if (ok)
            res = Py_False;
        else
            res = Py_True;
        Py_INCREF(res);
    }
}

This means that if Py_NE has not been overridden, the function tries the Py_EQ case. If the result is true, it returns Py_False; otherwise, it returns Py_True.

Rules of do_richcompare

After the above analysis and experiments, I believe that you have a very clear understanding of its implementation. Let me organize a table of rules below.

Python do_richcompare(v, w, op)

PriorityConditionDo
0v and w have different types, and w’s class is a strict subclass of v’s classTry Py_TYPE(w)->tp_richcompare with the operands and operator swapped
1v has a non-NULL slotTry Py_TYPE(v)->tp_richcompare
2w was not tried at priority 0 and has a non-NULL slotTry Py_TYPE(w)->tp_richcompare with the operands and operator swapped
3Every available slot returned Py_NotImplementedUse the switch-branch fallback

Other interesting findings

Now we have a clear understanding of how Python’s is and == operators are implemented. But do you still remember the surprising code at the beginning?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
>>> a = 1
>>> b = 1
>>> a is b
True
>>> a == b
True
>>> a = 114514
>>> b = 114514
>>> a is b
False
>>> a == b
True

They are both integers, so is there an essential difference between 1 and 114514? Let’s perform an experiment:

1
2
3
4
for i in map(str, range(-10, 260)):
    a = int(i)
    b = int(i)
    print(i, a is b)

From the output, we can see that a is b is True for the inclusive interval [-5, 256], but False for the other numbers in this experiment. This is related to the implementation of Python’s int type. In CPython 3, all int objects are represented as PyLongObject, regardless of their magnitude.

Let’s read Objects/longobject.c. In the _PyLong_Init function, a small_ints array is initialized for the interpreter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
for (Py_ssize_t i=0; i < NSMALLNEGINTS + NSMALLPOSINTS; i++) {
    sdigit ival = (sdigit)i - NSMALLNEGINTS;
    int size = (ival < 0) ? -1 : ((ival == 0) ? 0 : 1);

    PyLongObject *v = _PyLong_New(1);
    if (!v) {
        return -1;
    }

    Py_SET_SIZE(v, size);
    v->ob_digit[0] = (digit)abs(ival);

    tstate->interp->small_ints[i] = v;
}

Now look at the definition of this array:

1
2
3
4
5
6
    /* Small integers are preallocated in this array so that they
       can be shared.
       The integers that are preallocated are those in the range
       -_PY_NSMALLNEGINTS (inclusive) to _PY_NSMALLPOSINTS (not inclusive).
    */
    PyLongObject* small_ints[_PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS];

Its size is _PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS, and its range is [-5, 257).

1
2
3
4
/* interpreter state */

#define _PY_NSMALLPOSINTS           257
#define _PY_NSMALLNEGINTS           5

One C API path for creating an int is the PyLong_FromLong function, so let’s take a look at it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/* Create a new int object from a C long int */

PyObject *
PyLong_FromLong(long ival)
{
    PyLongObject *v;
    unsigned long abs_ival;
    unsigned long t;  /* unsigned so >> doesn't propagate sign bit */
    int ndigits = 0;
    int sign;

    if (IS_SMALL_INT(ival)) {
        return get_small_int((sdigit)ival);
    }

    if (ival < 0) {
        /* negate: can't write this as abs_ival = -ival since that
           invokes undefined behaviour when ival is LONG_MIN */
        abs_ival = 0U-(unsigned long)ival;
        sign = -1;
    }
    else {
        abs_ival = (unsigned long)ival;
        sign = ival == 0 ? 0 : 1;
    }

    /* Fast path for single-digit ints */
    if (!(abs_ival >> PyLong_SHIFT)) {
        v = _PyLong_New(1);
        if (v) {
            Py_SET_SIZE(v, sign);
            v->ob_digit[0] = Py_SAFE_DOWNCAST(
                abs_ival, unsigned long, digit);
        }
        return (PyObject*)v;
    }

#if PyLong_SHIFT==15
    /* 2 digits */
    if (!(abs_ival >> 2*PyLong_SHIFT)) {
        v = _PyLong_New(2);
        if (v) {
            Py_SET_SIZE(v, 2 * sign);
            v->ob_digit[0] = Py_SAFE_DOWNCAST(
                abs_ival & PyLong_MASK, unsigned long, digit);
            v->ob_digit[1] = Py_SAFE_DOWNCAST(
                  abs_ival >> PyLong_SHIFT, unsigned long, digit);
        }
        return (PyObject*)v;
    }
#endif

    /* Larger numbers: loop to determine number of digits */
    t = abs_ival;
    while (t) {
        ++ndigits;
        t >>= PyLong_SHIFT;
    }
    v = _PyLong_New(ndigits);
    if (v != NULL) {
        digit *p = v->ob_digit;
        Py_SET_SIZE(v, ndigits * sign);
        t = abs_ival;
        while (t) {
            *p++ = Py_SAFE_DOWNCAST(
                t & PyLong_MASK, unsigned long, digit);
            t >>= PyLong_SHIFT;
        }
    }
    return (PyObject *)v;
}

This function uses the IS_SMALL_INT macro to determine whether the number is in the range [-5, 256]. If it is, the function calls get_small_int and returns the result.

1
2
3
if (IS_SMALL_INT(ival)) {
    return get_small_int((sdigit)ival);
}

Looking at the get_small_int function, we can see that numbers in this range already exist in small_ints, so CPython does not allocate a new object for them.

1
2
3
4
5
6
7
8
9
static PyObject *
get_small_int(sdigit ival)
{
    assert(IS_SMALL_INT(ival));
    PyInterpreterState *interp = _PyInterpreterState_GET();
    PyObject *v = (PyObject*)interp->small_ints[ival + NSMALLNEGINTS];
    Py_INCREF(v);
    return v;
}

At this point, the surprising behavior has been explained: CPython caches the numbers in small_ints and reuses those objects instead of allocating new ones.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
>>> a = 256
>>> b = 256
>>> c = 257
>>> d = 257
>>> id(a)
1636339312944
>>> id(b)
1636339312944
>>> a is b
True
>>> id(c)
1636346998896
>>> id(d)
1636346999024
>>> c is d
False

It can be seen that this is indeed the case.

However, literal-based tests can also be affected by compiler constant reuse. For example, two occurrences of 257 in the same compiled code block may refer to the same constant object. The string-to-integer experiment above avoids that particular source of interference. Small-integer caching and constant reuse are CPython implementation details, so application code should always use == for numeric value comparison.

Conclusion

This study analyzed the differences and connections between is and ==. In general:

  • is compares object identity. In CPython, it does so by comparing the two object pointers.
  • == uses rich comparison. It falls back to pointer identity only if neither operand’s comparison implementation provides a result.

Python’s commonly used built-in types such as int, str, list, and dict provide type-specific implementations of tp_richcompare.

I searched the source code and found the following examples. This list may be incomplete.

Python typeRichcmp method
array.arrayarray_richcompare
bytearraybytearray_richcompare
bytesbytes_richcompare
cellcell_richcompare
codecode_richcompare
collections.dequedeque_richcompare
collections.OrderedDictodict_richcompare
complexcomplex_richcompare
datetime.timezonetimezone_richcompare
dictdict_richcompare
dict_itemsdictview_richcompare
dict_keysdictview_richcompare
floatfloat_richcompare
frozensetset_richcompare
instancemethodinstancemethod_richcompare
intlong_richcompare
listlist_richcompare
mappingproxymappingproxy_richcompare
methodmethod_richcompare
method-wrapperwrapper_richcompare
rangerange_richcompare
re.Patternpattern_richcompare
setset_richcompare
sliceslice_richcompare
strPyUnicode_RichCompare
tupletuplerichcompare
weakrefweakref_richcompare

The Python language has accelerated my development cycle. Personally, I like Python very much, but I had not previously delved into many of its details. After reading the CPython source code, I gained a better understanding of many behaviors that I had once found surprising.

使用 Hugo 构建
主题 StackJimmy 设计