== compares values: are these two things equal? is compares identity: are these two names pointing at the very same object in memory? For almost everything, you want ==. The one place you should always use is is comparing with None. Mixing them up usually works by accident, until the day it does not.
Equal versus the same#
a = [1, 2, 3]
b = [1, 2, 3]
c = a
a == b # True - same contents
a is b # False - two separate lists that happen to match
a is c # True - c is another name for the same list
print(id(a), id(b), id(c)) # b has a different id; a and c share one
id() returns a number unique to each live object. is is exactly id(x) == id(y). Two lists built separately are two objects; assigning one name to another makes two names for one object. Change c and a changes with it, because there is only one list.
None: always is#
if result is None: # correct
if result == None: # works, usually - but wrong
if not result: # different again: also true for 0, "", [], False
There is exactly one None object, so identity is the precise question. == None asks the object how it compares with None, which a class can override to return anything; some libraries’ objects return arrays or raise from ==. is None cannot be intercepted. The style guide for Python, PEP 8, says to use is, and linters flag the alternative.
The third form, not result, is a different check altogether. It is right when you mean “empty or missing” and wrong when zero or an empty string is a legitimate value.
Why is sometimes appears to work for values#
a = 5
b = 5
a is b # True - but not for the reason you think
a = 1000
b = 1000
a is b # False, or True, depending on how the code was run
CPython keeps one shared object for each small integer, roughly -5 to 256, and reuses it. So two variables set to 5 point at the same object and is says True. Larger integers, and most strings and floats, are created fresh, so is says False — except when the compiler happens to combine identical constants in the same file, which makes it True again. None of this is guaranteed by the language; it is an implementation detail.
Python 3.8 and later warn about this: SyntaxWarning: "is" with a literal. Did you mean "=="?. Heed it.
Where is belongs#
x is None,x is not Nonex is True,x is False— only when you specifically need the boolean object and not anything truthy. Usuallyif x:is what you want.- Sentinel objects: a unique marker meaning “not provided”, when None is a valid value:
_MISSING = object()
def get(key, default=_MISSING):
if default is _MISSING:
raise KeyError(key)
return default
- Explicit identity checks: “is this the same object I put in the cache?” or detecting aliasing.
Everywhere else, ==.
Where == belongs#
- Numbers, strings, lists, tuples, dictionaries, sets:
if name == "admin",if total == 0. - Comparing instances of your own classes by their contents.
- Anything a user typed, anything read from a file, anything computed.
Defining == for your own classes#
By default, two instances of a class are equal only if they are the same object — == falls back to is. To compare by contents, define __eq__:
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))
Point(1, 2) == Point(1, 2) # True
Point(1, 2) is Point(1, 2) # False, always
A dataclass generates __eq__ for you, which is one of the reasons to use one. If you define __eq__ you should define __hash__ as well, or the class stops working as a dictionary key.
Performance#
is is a single pointer comparison and cannot be overridden, so it is marginally faster. That is not a reason to use it for values; the difference is negligible and the semantics are wrong. Use it where identity is the question and nowhere else.
Questions people ask#
Is “is not” the same as “not … is”?
Yes: x is not None and not (x is None) are equivalent, and the first reads better.
Why does == on two NumPy arrays not return a boolean?
Because NumPy overrides == to compare element by element and return an array. That is precisely why == None is unsafe: a library can make it mean anything.
Does is work for comparing types?
type(x) is int is a valid identity check, since each class is a single object. isinstance(x, int) is usually better because it respects subclasses.
What about != and is not?
Same rules: != for values, is not for None and identity.
Where to go next#
- Shallow vs deep copy — where identity matters most.
- Python operators — the full set.
- Dataclasses explained — free __eq__ for your classes.