Python’s not-equal operator is !=. There is no <> - it existed in Python 2 and was removed - and is not is a different operator that does something else entirely. != compares values: are these two things equal in content? is not compares identity: are these two names pointing at the same object in memory? For small integers and interned strings, CPython reuses objects, so the two operators agree and everything looks fine. Then a value comes in from a file, a socket, or a database, the object is no longer shared, and code that worked for years starts returning the wrong answer with no error at all.
The literal question has a one-character answer. The reason this keeps getting asked is the operator sitting next to it, which looks like English and does not mean what English suggests.
Table of contents
- The operator
- != compares values, is not compares identity
- When is not is correct
- Where it actually goes wrong
- A rule that covers every case
- How this fits the rest of the stack
- FAQ
The operator
1 != 2 # True
"a" != "a" # False
[1, 2] != [1, 2] # False - same contents
None != False # True - different objects, different values
# Removed in Python 3. Do not look for it.
1 <> 2 # SyntaxError
!= is the inverse of == and delegates to the same machinery. If a class defines __eq__, Python derives != from it automatically - you almost never need to write __ne__ yourself, which was required in Python 2 and is now a small piece of obsolete advice that still circulates.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
Point(1, 2) != Point(1, 2) # False - dataclass gives you __eq__
Point(1, 2) != Point(3, 4) # True
!= compares values, is not compares identity
This is the whole article.
a = [1, 2, 3]
b = [1, 2, 3]
a != b # False - same contents
a is not b # True - different objects!
c = a
a is not c # False - same object
Two lists with identical contents are equal and are not identical. != asks about contents. is not asks whether they are literally the same object in memory.
Now the part that makes this dangerous - CPython caches small integers and short strings, so identity accidentally matches value:
x = 256
x is not 256 # False - cached, works by luck
x = 257
x is not 257 # True - not cached. Same code, different answer.
a = "hello"
a is not "hello" # False - interned, works by luck
b = "hello world!"
b is not "hello world!" # may be True - not interned
This is why the bug survives. You write if status is not "active", test it with a literal, and it works - because both are the same interned string object. Then the value arrives from json.loads() or a database driver, it is a fresh object, is not returns True, and your condition inverts. Nothing raises. The behaviour just changes.
Modern Python at least warns you: SyntaxWarning: "is not" with a literal. Did you mean "!="?. Do not ignore it - it is one of the most valuable warnings in the language.
When is not is correct
There is one clear case, and it is common enough to matter: singletons.
if value is not None: # correct - the idiomatic check
...
if flag is not True: # legal, but usually you want: if not flag
...
if sentinel is not MISSING: # correct - a unique sentinel object
...
None is a singleton: exactly one instance exists for the life of the program. So identity and equality are always the same for it, and is not None is both correct and faster - no __eq__ call.
It is also more correct than != None, because a class can override __eq__ and lie:
import numpy as np
arr = np.array([1, 2, 3])
arr != None # array([True, True, True]) - elementwise! Not a bool.
arr is not None # True - unambiguous
arr != None returns an array, and putting an array in an if raises ValueError. is not None cannot be overridden and always answers the question you asked. The rule: use is not None for None, != for everything else.
Where it actually goes wrong
The realistic shapes of this bug:
# Works with a literal, breaks with parsed JSON.
if payload["status"] is not "active":
reject()
# Works in tests with small ints, breaks with real IDs.
if user_id is not 1:
...
# Works until the value comes from a database driver.
if row["count"] is not 0:
...
Every one of these passes tests. Test data uses literals, literals are interned or cached, identity matches. Production data comes from a parser, a socket, or a driver - fresh objects every time - and the condition silently inverts.
The failure is total and silent: no exception, no log line, just a branch that stops being taken. This is why the SyntaxWarning matters and why is with a non-singleton should never survive review.
One more variant worth naming - checking for empty:
if items != []: # works, but creates a list to compare against
if items is not []: # always True. [] is a new object every time.
if items: # idiomatic and correct
items is not [] is always True, unconditionally, because the literal [] constructs a brand new list that cannot be the same object as anything. It is a condition that does nothing and looks like it does something.
A rule that covers every case
- Comparing to
None?is None/is not None. Always. - Comparing to
TrueorFalse? Usually justif flag:orif not flag:. - Comparing to a unique sentinel you created for the purpose?
is/is not. - Anything else - numbers, strings, lists, objects?
==/!=. - Checking for empty?
if not items:, not a comparison to[].
If you follow rule 4 for everything that is not a singleton, this class of bug cannot occur in your code. And if you see a SyntaxWarning about is with a literal, it is not noise - it is Python catching a real bug before your users do.
How this fits the rest of the stack
A condition that silently inverts on real data is the worst kind of bug: it does not crash, it does not log, and it passes every test written with literals. The only defence is knowing which question each operator asks. The same is true of a cloud bill - the wrong number does not announce itself, it just recurs. The RunxBuild hosting calculator puts the compute, database, storage, and bandwidth side by side so the total is something you verified, and the RunxBuild dashboard is where the team sees actual usage.
Useful related references:
- Python Iterate a List: for, enumerate, list comprehension, and map
- Private Methods in Python: There Are None, and That Is Fine
- Build a Python VPN Client: When It Makes Sense and When It Doesn’t
- Python services on RunxBuild
FAQ
What is the not equal operator in Python?
!=. It compares values and is the inverse of ==. Python 2’s <> was removed and raises a SyntaxError in Python 3. If a class defines __eq__, Python derives != from it automatically, so you rarely need to write __ne__ yourself - that requirement was a Python 2 thing that still shows up in old advice.
What is the difference between != and is not in Python?
!= compares values - do these two things have equal contents. is not compares identity - are these two names pointing at the same object in memory. Two lists with identical contents are == but not is. Use is not only for singletons like None; use != for everything else, because identity comparison on ordinary values is a bug waiting for non-interned data.
Why does is not work with strings sometimes but not always?
Because CPython interns short strings and caches small integers, so identical literals often end up as the same object and identity comparison accidentally matches value. That stops being true for longer strings, computed values, and anything parsed from JSON or a database - which is why code using is with a literal passes tests and fails in production. Python emits a SyntaxWarning for exactly this reason.
Should I use != None or is not None in Python?
is not None. None is a singleton, so identity is the correct and faster check. It is also safer: a class can override __eq__ so that != None returns something unexpected - NumPy arrays return an elementwise array rather than a boolean, which then raises when used in an if. is not None cannot be overridden and always answers the question you asked.
Does Python have a <> operator for not equal?
No. <> existed in Python 2 as an alternative spelling and was removed in Python 3, where it is a SyntaxError. Use !=. If you are reading old code or documentation that shows <>, it predates Python 3 and may contain other Python 2 assumptions worth checking, such as the requirement to define __ne__ alongside __eq__.