is None vs == None in Python: What’s the Difference?

Use is None. The difference between is None vs == None in Python is that is checks whether a name points to the one and only None object, while == asks the object’s __eq__ method, which a class can rewrite to say anything:

result is None    # identity: always correct
result == None    # equality: asks __eq__, can be fooled

I ran these snippets on Python 3.12.5, NumPy 2.5.3 and pandas 3.0.6; outputs and screenshots are real.

How is None and == None compare on plain values

On built-in values, both checks agree, which is why == None still shows up in so much code:

order_id = None
discount = 0

print(order_id is None, order_id == None)
print(discount is None, discount == None)

backup = None
print(backup is order_id)

Output:

True True
False False
True
Command Prompt output showing is None and == None both returning True for None and False for 0
Both checks agree on None and on 0, and both names point to one object.

None is a singleton: every None in your program is the same object, so an identity check asks exactly the right question.

For the full how-to, see checking if a variable is None in Python, and the difference between is and == in Python covers identity beyond None.

Why can == None give the wrong answer with __eq__?

x == None calls x.__eq__(None), so the class decides the result. Here’s a standard-library object that claims to equal None and a custom class that crashes:

from unittest.mock import ANY

print("ANY == None:", ANY == None)
print("ANY is None:", ANY is None)

class Order:
    def __init__(self, total):
        self.total = total

    def __eq__(self, other):
        return self.total == other.total

order = Order(149.99)
try:
    print(order == None)
except AttributeError as err:
    print("order == None failed:", err)
print("order is None:", order is None)

Output:

ANY == None: True
ANY is None: False
order == None failed: 'NoneType' object has no attribute 'total'
order is None: False
Command Prompt output where unittest.mock.ANY equals None and a custom Order class raises AttributeError on == None
ANY == None wrongly returns True and Order.__eq__ crashes; is None is right both times.

I’ve hit that second bug in real code. You can’t override is, so it never calls your method. The same goes for != None and the not equal operator in Python.

NumPy arrays and pandas Series with == None

With arrays, == compares element by element and returns an array, not one True or False, so an if fails:

import numpy as np
import pandas as pd

prices = np.array([19.99, None, 7.50], dtype=object)
print(prices == None)
try:
    if prices == None:
        pass
except ValueError as err:
    print("ValueError:", err)

cities = pd.Series(["Austin", None, "Denver"])
print((cities == None).tolist())
print(cities.isna().tolist())

Output:

[False  True False]
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
[False, False, False]
[False, True, False]
Command Prompt output of NumPy == None returning an element-wise array, a ValueError in an if statement, and pandas == None returning all False
NumPy compares per element and rejects the array in an if; in pandas only isna() finds the missing city.

pandas stores the missing city as a missing-value marker, so == None is False everywhere. You’ll want isna() instead. See checking for NaN in Python and dropping rows with NaN in pandas for the rest of the cleanup.

Write is not None instead of != None or not x

The negative form follows the same rule: if x is not None:. Avoid if not x: when you mean “missing”, because 0, 0.0 and an empty string are falsy too.

With if not discount:, a $0.00 discount would be treated like no discount at all. The if not condition in Python explains how falsy values behave, and checking that a variable is not null shows the pattern.

Ruff E711 flags comparison to None

PEP 8 says comparisons to singletons like None should use is or is not, and linters can enforce it.

Ruff 0.17’s default rules passed this file in my test, so you’ll need --select E711. Then it flags both lines in this function with an optional argument defaulting to None:

def apply_discount(price, coupon=None):
    if coupon == None:
        return price
    if coupon.get("percent") != None:
        return round(price * (1 - coupon["percent"] / 100), 2)
    return price

Output:

checkout.py:2:18: E711 Comparison to `None` should be `cond is None`
checkout.py:4:33: E711 Comparison to `None` should be `cond is not None`
Found 2 errors.
2 unsafe fixes available with `--fix --unsafe-fixes`.
Command Prompt output of ruff check reporting E711 comparison to None on two lines of checkout.py
Ruff flags both comparisons and suggests the is forms.

is None vs == None at a glance

CheckTestsCalls __eq__Use it for
x is NoneSame objectNoEvery None check
x == NoneEqual valueYesAvoid (E711)
x is not NoneNot the same objectNoValue is present

If you handle missing data often, you might like these:

Frequently asked questions

Should I use is None or == None in Python?

Use is None. A class’s __eq__ method can’t change its answer.

Is is None faster than == None?

Slightly. is compares object identity directly, while == goes through the comparison methods first.

Why does x == None return True when x isn’t None?

Its __eq__ method says so, as unittest.mock.ANY does on purpose.

Is is not None the same as not x is None?

Same result, but PEP 8 recommends is not None because it reads better.

What does ruff E711 mean?

E711 means a comparison to None uses == or !=. Change it to is or is not. Enable it with --select E711.

Where are is and is not documented?

See identity comparisons in the Python language reference.