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.
- 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.
- 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 basks — is this the very same object?a == basks — 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:
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 matchThe 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
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.
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:
iscannot be overridden. There is no__is__method; the operator compiles, in CPython, to anIS_OPinstruction that compares two pointers — which is an implementation detail, not the definition of the operator.isdoes 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:
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
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:
print(int("256") is int("256")) # True
print(int("257") is int("257")) # FalseThe 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:
def first(): return 1000
def second(): return 1000
print(first() is second()) # TrueThe 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.
import sys
ident = "hello_world"
built = "".join(["hello", "_", "world"])
print(built is ident) # False
print(sys.intern(built) is ident) # TrueThe 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
nan = float("nan")
print(nan == nan) # False
print(nan is nan) # True
print(nan in [nan]) # True
print([nan] == [nan]) # TrueThe 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).
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:
print(float("nan") in [nan]) # FalseThe 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.
class AlwaysEqual:
def __eq__(self, other):
return True
obj = AlwaysEqual()
print(obj == None) # True
print(obj is None) # FalseAnd __eq__ is not even required to return a boolean:
class Weird:
def __eq__(self, other):
return "not a boolean"
print(bool(Weird() == 1)) # True — a non-empty string is truthyif 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
print(id([]) == id([])) # TrueNo 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.
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
== 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
Two identical literals compared with is gave True. Can that be relied on?
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.
There is a value for which x == x is false. Which, and what follows from it?
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.
Is id() unique?
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
1000 is 1000 is True because of the number cache
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.
All identical string literals are one object
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.
nan is not equal to itself, so it cannot be found in a list
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.
id(a) == id(b) is the same as a is b
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.
is is faster, so use it wherever you can
"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
| Version | Change | What this means for your code |
|---|---|---|
| 3.8 | The 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.12 | PEP 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.14 | The 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
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
Check yourself
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.
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- The two operators answer two different questions.
isasks: 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. Soisbelongs 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:
iscomes 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 why1000 is 1000is 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
iswith a literal by itself. Comparing pointers is how CPython implements identity, and another implementation may do it differently. The cost:istakes 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 "writeis" does not follow.
In fact
- 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. - 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 throughsys.intern. There is no converse rule either —"".join([s])with one element returnssitself. - It is found. The reference gives the exact equivalence:
x in yisany(x is e or x == e for e in y), and identity is checked FIRST. The very samenanobject is found byis; a differentnanof the same value is not. For the same reasonnanworks as a dictionary key as long as you put in and take out the same object. - Only while both objects are alive. The documentation states it outright: two objects with non-overlapping lifetimes may have the same
id. Henceid([]) == id([])being True: the first list dies before the second is created, and the address goes to the second. - "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,Enummembers and your ownobject()are examples of that rule. On equal 200-character strings the difference betweenisand==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 valuesisanswers wrongly — and does it fast.
By version
- 3.8
- The compiler starts issuing a
SyntaxWarningforiswith 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.12
- PEP 683:
None,True,Falseand small integers become immortal and stop counting references at all. This affectsisin no way, but it changes whatsys.getrefcountprints: instead of a meaningful number it returns a sentinel constant. Reading it as "how many references" is no longer possible.< - 3.14
- The 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.<
What is covered
- Base: the same object, or the same contents
- Mechanism 1: what each of the two does
- Mechanism 2: where `is` is the right tool
- Mechanism 3: where `is` is True for reasons you did not write
- Mechanism 4: `nan` — the one value where everything diverges
- Mechanism 5: `is None`, not `== None`
- Mechanism 6: `id` is not always unique
- Deeper: what it costs
- How to answer in an interview
- Next they ask
- Common misconceptions
- Version history
- Practice
- Check yourself
- What measured this
Sources & further reading
7 SOURCES
- 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
- 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
- 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
- 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
- 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
- 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/
- 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