Deep Engineering
Intermediate·Published·3.11 · 3.12 · 3.13 · 3.14·25 MIN

is versus ==: one asks about identity, the other calls a method — and half the famous surprises are about neither

“is compares objects, == compares values” is true and useless: it does not answer the question people actually have, which is why 1000 is 1000 comes out True. There are three different answers — the small-integer cache, the compiler merging equal constants, and string interning — and code may rely on none of the three.

Full technical treatment

TL;DR

The two operators answer two different questions. is asks: is this the very same object? == asks: do these values count as equal? The first is a contract of the language — "true if and only if x and y are the same object" — and there is nothing to override: no __is__ method exists. The second is a call to __eq__, that is, an ordinary method, which may return anything and cost anything. So is belongs where the API's own contract is about identity: with objects that exist in a single copy, and with sentinels of your own created for exactly this kind of comparison.

Hence the main consequence: is comes out true where you never wrote it — and not for one reason but for three different ones. The small-integer cache, from −5 to 256 inclusive. The compiler merging equal constants, which is why 1000 is 1000 is True inside one file and False across two. String interning: a literal shaped like an identifier turns out to be one object for the whole program, while a string built at run time cannot be relied on. Telling the three apart matters more than knowing each, because none of them is promised by the language.

Beyond that is what separates knowing from having read. The cache bounds are the same across the 3.11–3.14 builds checked here, and since 3.8 the compiler warns about is with a literal by itself. Comparing pointers is how CPython implements identity, and another implementation may do it differently. The cost: is takes 11.8–12.1 ns regardless of anything; == on equal strings of two hundred thousand characters takes 4701 ns, four hundred times more (3.13.7). But on short strings the difference is merely twofold, and "write is" does not follow.

Where to start
Before this lesson it is enough to understand
  • a name in a program is bound to an object, and one object can have several names;
  • two different objects can hold the very same contents;
  • a class can define its own behaviour for operators through double-underscore methods.
You do not need to know in advance
  • the small-integer cache, constant merging by the compiler, string interning;
  • __eq__, id(), sys.intern, nan, immortal objects.

Base: the same object, or the same contents

Before getting to caches and interning it is worth naming, in ordinary words, what each of the two operators asks. There are exactly two questions, and they are different:

  • a is b asks — is this the very same object?
  • a == b asks — do these values count as equal?

The difference shows on three variables, where two names point at one object and the third at a separate object with the same contents:

PYTHON
first = [1, 2, 3]
second = first              # a second name for the very same object
third = [1, 2, 3]           # a separate object with the same contents
 
print(first is second)      # True  — one object
print(first is third)       # False — two objects
print(first == third)       # True  — the contents match

The third line is the whole point: first and third are equal and not identical. The same thing shows from the other side — append an item to first and second changes with it, because it is one object, while third stays as it was.

And here is the question this lesson is about: why do two identical literals sometimes turn out to be one object? You wrote 1000 twice, and is says True — even though by the same logic that should be two separate objects, like first and third.

That is already enough to answer the basic interview question: is is about identity, == is about equality of values, and one cannot stand in for the other. Everything below is about where that extra True comes from, why it has three different causes, and why code may rely on none of them.

Mechanism 1: what each of the two does

language contractLanguage guarantee: is is true if and only if it is the same object. Comparing pointers is already implementation.

The definition of is in the reference is one sentence, and it says nothing about values.

The operators is and is not test for an object's identity: x is y is true if and only if x and y are the same object. An Object's identity is determined using the id() function.

Language reference, identity comparisons

The data model says what identity is: "An object's identity never changes once it has been created; you may think of it as the object's address in memory." For CPython that is not "you may think of it" but literally so — an implementation note right below says as much.

The line between those two statements is worth holding on to for the rest of the lesson. The language's contract is "the same object or not"; identity being implemented as an address in memory is a property of CPython, not a promise of the language. "is compares addresses" is a correct answer to "how is it done"; to "what does it mean" it is no answer at all.

Three things follow at once:

  • is cannot be overridden. There is no __is__ method; the operator compiles, in CPython, to an IS_OP instruction that compares two pointers — which is an implementation detail, not the definition of the operator.
  • is does not depend on type or size. Comparing two hundred-megabyte objects is exactly as cheap as comparing two numbers.
  • == is the opposite on every count: it can be overridden, and it depends on both type and size. It is a call to __eq__, which may return a non-boolean, walk a million elements, or go over the network.

Mechanism 2: where is is the right tool

"Use is only with None" is short and wrong: the rule is not about None, it is about identity as a contract. Some objects come with an API promise of being the only one of their kind, and those are exactly the ones to ask about with is — comparing by value is either redundant or dangerous. A run of bench/identity/contract.py, block 3:

None is None                                   True
NotImplemented is NotImplemented               True
Ellipsis is ...                                True
Color.RED is Color(1)                          True

The list continues with your own sentinel — the case where nothing else will do:

PYTHON
MISSING = object()
 
def find(mapping, key):
    got = mapping.get(key, MISSING)
    return "no such key" if got is MISSING else got
 
find({"k": None}, "k")      # None — the key is there, its value is None
find({}, "k")               # 'no such key'

== MISSING fails here for the same reason == None does: equality asks a foreign object, while a sentinel is a question about identity.

Where the rule's boundary runs matters. It is not the four-line list above: None, NotImplemented, Ellipsis and Enum members are examples, not an exhaustive catalogue. The rule is general: is belongs with any object that exists in a single copy, and with any sentinel created specifically to be compared by identity. Your own MISSING = object() is on the list on exactly the same terms as None — and so is any other sentinel you invent tomorrow.

is True and is False are NOT on this list, singletons though they are: x is True rejects 1, a non-empty string and everything else truthy. That is not a question about identity, it is a question about truthiness asked with the wrong operator.

Mechanism 3: where is is True for reasons you did not write

implementation detail · CPython 3.13The small-integer cache, constant merging and interning belong to the build. Code must not rely on them.

This is where the real confusion lives. "is compares objects" does not explain why 1000 is 1000 is True — and what explains it is three DIFFERENT mechanisms of three different strengths.

Under each row of the picture there is a mark saying what the answer rests on: a promise in the reference, the way CPython works, or whether the literals landed in one compilation. Only the first can be carried into code.

Each in turn.

The small-integer cache. The numbers from −5 to 256 are created at interpreter startup and handed out ready-made. Testing this needs care: the numbers have to be ones the compiler cannot work out in advance:

PYTHON
print(int("256") is int("256"))     # True
print(int("257") is int("257"))     # False

The bounds are the same on 3.11, 3.12, 3.13 and 3.14.7. But this is a CPython detail, not a promise of the language.

Constant merging. And this one is not about numbers at all:

PYTHON
def first():  return 1000
def second(): return 1000
 
print(first() is second())          # True

The compiler merges equal constants within one compilation — even when they sit in different functions. Move one function into another file and the same code returns False. So the first True is a property of how the source was BUILT, not a property of numbers.

String interning. The rule is simple: a literal shaped like an identifier is interned — one object for the whole program, other files included. A string built at run time cannot be relied on to be interned automatically, even when it looks like an identifier. The converse fails too: is sometimes says True without interning, when the operation returned the same object. Verified: "".join([s]) with one element, the full slice s[:] and str(s) all give the original string back, and the empty string and single-character latin-1 strings are cached singletons outright.

PYTHON
import sys
 
ident = "hello_world"
built = "".join(["hello", "_", "world"])
 
print(built is ident)               # False
print(sys.intern(built) is ident)   # True

The documentation also says what it is for: "the key comparisons (after hashing) can be done by a pointer compare instead of a string compare." Interning exists to make dictionaries fast, not so that you can write is with strings.

The compiler warns about this itself, and has since 3.8:

SyntaxWarning: "is" with 'str' literal. Did you mean "=="?

Mechanism 4: nan — the one value where everything diverges

PYTHON
nan = float("nan")
 
print(nan == nan)          # False
print(nan is nan)          # True
print(nan in [nan])        # True
print([nan] == [nan])      # True

The first line is a requirement of the floating-point standard. The third is explained by the reference, which gives an exact equivalence for membership tests.

For container types such as list, tuple, set, frozenset, dict, or collections.deque, the expression x in y is equivalent to any(x is e or x == e for e in y).

Language reference, membership test operations

Identity is checked first — that is written into the order of the or operands. So the very same nan is found in the list, while a different nan of the same value is not:

PYTHON
print(float("nan") in [nan])    # False

The fourth line rests on the same order: a list compares its elements by identity first and only then by equality. That, however, is how CPython works rather than a promise of the language — the reference describes membership tests this way, not list comparison.

The same rule explains why nan works as a dictionary key: if you put it in and take it out with the same object, identity does the job.

Mechanism 5: is None, not == None

A well-known rule with exactly one reason behind it: == is a method call, and a method can lie.

PYTHON
class AlwaysEqual:
    def __eq__(self, other):
        return True
 
obj = AlwaysEqual()
print(obj == None)      # True
print(obj is None)      # False

And __eq__ is not even required to return a boolean:

PYTHON
class Weird:
    def __eq__(self, other):
        return "not a boolean"
 
print(bool(Weird() == 1))       # True — a non-empty string is truthy

if x == None: will then quietly take the wrong branch. is None cannot be intercepted: it asks about identity, and there is no method for that question.

The same goes for is True and is False, but with the opposite conclusion: do not write those. x is True rejects 1, a non-empty string and everything else truthy — almost always a bug rather than an intent.

Mechanism 6: id is not always unique

PYTHON
print(id([]) == id([]))     # True

No magic: the first list dies before the second is created, and the address goes to the second. The documentation states this outright.

This is an integer which is guaranteed to be unique and constant for this object during its lifetime. Two objects with non-overlapping lifetimes may have the same id() value.

Built-in functions, id()

The practical conclusion: id(a) == id(b) is not a replacement for a is b but a worse version of it. It gives the same answer only while both objects are alive; an id saved into a variable means nothing.

Deeper: what it costs

measured observationbench/identity/cost.py, CPython 3.13.7. Absolute nanoseconds depend on the machine; what matters is how == grows with length.

The picture does not say "write is". It shows the shape of each cost.

is has a constant cost: two pointers compare the same whether the operands are numbers or two-hundred-thousand-character strings. == grows with the length, because equal strings have to be read to the end: there is no difference to stop at. Strings that differ at the first character compare faster than equal ones — 19.58 against 24.62 ns. In the growth table the same comparison gave 24.29: the numbers are comparable only inside their own block, and a quarter of a percent between blocks is machine noise.

One row stands apart: an object with its own __eq__ written in Python costs 101.83 ns against 24.68 ns for an object without one — four times more. A Python method costs more than the comparison the interpreter does itself, and that is worth remembering in a hot loop — but the fix is not swapping in is, it is not doing extra comparisons in the loop.

How to answer in an interview

The short answer: is asks whether this is the same object; it cannot be overridden, and its cost does not depend on the object. == calls __eq__, an ordinary method that may return anything. is belongs where the API contract is identity itself: with any singleton object and any sentinel created for such a comparison — None, NotImplemented, Ellipsis, Enum members, your own object().

That is enough for a correct answer. What follows is what you add when the interviewer digs.

If the interviewer digs deeper

The first thing worth separating without being asked: comparison by identity is a contract of the language, while the address in memory is a CPython detail. "is compares addresses" answers "how is it done", not "what does the operator promise"; on another implementation identity may be arranged differently, and the contract will not change because of it.

If they ask about 1000 is 1000, the right answer names all three mechanisms without mixing them up: the small-integer cache ends at 256, constant merging works within one compilation, interning is about strings shaped like identifiers. And add that the compiler has warned about it since 3.8, putting it exactly this way: "These can often work by accident in CPython, but are not guaranteed by the language spec."

And one thing that is easy to overdo: the cost. Saying "is is four hundred times cheaper" turns one measurement into a general rule — the four hundred came from equal strings of two hundred thousand characters on one machine, and on short strings the difference is merely twofold. The rule is written from the boundary rather than from the number: is costs the same always, == grows with the length, and the choice between them is made by which question you are asking, not by price.

Next they ask

Next they ask

Two identical literals compared with is gave True. Can that be relied on?

Short answer

No. The True comes not from what the code says but from the interpreter having reused the object — an implementation detail, not a language guarantee, and on a different build or in a different context it may not hold.

Next they ask

There is a value for which x == x is false. Which, and what follows from it?

Short answer

nan — the one value where everything diverges: x == x is False while x is x is still True. Hence the practical consequence about containers: the lookup is built so that a nan in a list is found, even though it is not equal to itself.

Next they ask

Is id() unique?

Short answer

Within the object's lifetime, yes. But once an object is freed the same id can go to another one, so storing an id and comparing it later is a way to get a false match.

Common misconceptions

Claim

1000 is 1000 is True because of the number cache

Actually

The cache ends at 256 — int("257") is int("257") is False. The True in source comes from something else: the compiler merges equal constants within ONE compilation, and it does so even for literals in different functions of the same file. The same literal from another file gives False. Checked on 3.11, 3.12, 3.13 and 3.14.7.

Claim

All identical string literals are one object

Actually

Only the ones shaped like identifiers. "hello_world" is interned and matches even across files; "hello world" with a space is not. And a string built at run time cannot be relied on to intern itself: "".join(["hello", "_", "world"]) gives a new object until it is passed through sys.intern. There is no converse rule either — "".join([s]) with one element returns s itself.

Claim

nan is not equal to itself, so it cannot be found in a list

Actually

It is found. The reference gives the exact equivalence: x in y is any(x is e or x == e for e in y), and identity is checked FIRST. The very same nan object is found by is; a different nan of the same value is not. For the same reason nan works as a dictionary key as long as you put in and take out the same object.

Claim

id(a) == id(b) is the same as a is b

Actually

Only while both objects are alive. The documentation states it outright: two objects with non-overlapping lifetimes may have the same id. Hence id([]) == id([]) being True: the first list dies before the second is created, and the address goes to the second.

Claim

is is faster, so use it wherever you can

Actually

"Wherever you can" is not a list but a rule: any object that exists in a single copy, and any sentinel created to be compared by identity. None, NotImplemented, Ellipsis, Enum members and your own object() are examples of that rule. On equal 200-character strings the difference between is and == is merely twofold (12.01 against 24.29 ns, 3.13.7); the four-hundredfold figure belongs to strings of two hundred thousand characters, which rarely turn up in comparisons. The main point is elsewhere: on DIFFERENT objects with equal values is answers wrongly — and does it fast.

Version history

VersionChangeWhat this means for your code
3.8The compiler starts issuing a SyntaxWarning for is with a literal — stating the reason outright: in CPython such checks often work by accident but are not guaranteed by the spec. In practice this means half the mistakes in this lesson are now caught by the compiler, provided warnings are not silenced.
3.12PEP 683: None, True, False and small integers become immortal and stop counting references at all. This affects is in no way, but it changes what sys.getrefcount prints: instead of a meaningful number it returns a sentinel constant. Reading it as "how many references" is no longer possible.
3.14The value of that sentinel changes: on 3.12 and 3.13 sys.getrefcount(None) gives 4294967295, on 3.14.7 it gives 3221225472. No identity check is affected. This number cannot be read as a reference count in any version: it is a sentinel, and its value differs from version to version.

Practice

Two exercises. Answer first, then check against the real output: in both, the right answer comes from a recorded run rather than from an editor.

Practice · predict the output

The numbers are computed rather than written as literals, so constant folding stays out of it. What does this code print?
a = int("257")
b = int("257")
x = int("255")
y = int("255")

print(a == b)
print(a is b)
print(x is y)

Practice · estimate

Two equal strings of thirty thousand characters: comparison with == against is. How many times more expensive is ==?
times

Check yourself

Question 1 of 5

In one file: def first(): return 1000 and def second(): return 1000. What does first() is second() return, and why?

What measured this

The numbers in this article come from these scripts. Each one opens from here, together with the record of the run: what it was measured on, what came out, and with what spread.

Sources & further reading

7 SOURCES

  1. Language reference — identity comparisonsOfficial documentation. The definition of the operator, verbatim: «The operators is and is not test for an object's identity: x is y is true if and only if x and y are the same object. An Object's identity is determined using the id() function». Everything else follows from it: there is nothing to override, and the operator does not depend on types.https://docs.python.org/3.14/reference/expressions.html#is-not
  2. Data model — the identity of an objectOfficial documentation. Where the claim that identity never changes comes from: «Every object has an identity, a type and a value. An object's identity never changes once it has been created; you may think of it as the object's address in memory». The same page adds an implementation note: «For CPython, id(x) is the memory address where x is stored».https://docs.python.org/3.14/reference/datamodel.html#objects-values-and-types
  3. Built-in functions — id()Official documentation. The lesson's key caveat about comparing ids, verbatim: «This is an integer which is guaranteed to be unique and constant for this object during its lifetime. Two objects with non-overlapping lifetimes may have the same id() value». Hence id([]) == id([]) coming out True.https://docs.python.org/3.14/library/functions.html#id
  4. Language reference — membership test operationsOfficial documentation. Why nan is found inside a list that contains it. The reference gives the exact equivalence: «For container types such as list, tuple, set, frozenset, dict, or collections.deque, the expression x in y is equivalent to any(x is e or x == e for e in y)». Identity is checked FIRST — that is written into the order of the or operands.https://docs.python.org/3.14/reference/expressions.html#membership-test-operations
  5. sys.intern — the table of interned stringsOfficial documentation. What the function does and why: «Enter string in the table of “interned” strings and return the interned string — which is string itself or a copy», and on the benefit: «the key comparisons (after hashing) can be done by a pointer compare instead of a string compare». The same page states the fact behind half the observations in this lesson: «Normally, the names used in Python programs are automatically interned».https://docs.python.org/3.14/library/sys.html#sys.intern
  6. PEP 683 — Immortal Objects, Using a Fixed RefcountPEP. Eric Snow and Eddie Elizondo, accepted for 3.12. This is where the fact that None, True, False and small integers stop counting references comes from: an immortal object's refcount never changes, and sys.getrefcount returns a sentinel constant. It affects identity checks in no way — the PEP is here precisely to explain why that number must not be read as “how many references”.https://peps.python.org/pep-0683/
  7. What's New in Python 3.8 — the warning on is with a literalOfficial documentation. The source for the version and for the wording of the reason: «The compiler now produces a SyntaxWarning when identity checks (is and is not) are used with certain types of literals (e.g. strings, numbers). These can often work by accident in CPython, but are not guaranteed by the language spec». “Work by accident” is exactly what this lesson is about.https://docs.python.org/3/whatsnew/3.8.html