Skip to content
← Back to explainers

Python explainer

When to Use is and When to Use ==

About 4 min to read. Allow extra time to work through examples.

Use is to check for the same object and == to check for the same value. Choose which dataclass fields count for ==, and keep the hashes of equal values equal.

This article uses Python. You can make it your practice mode to browse related content across the site.

Your code reads a status from a request and decides whether to send an email:

status = "".join(["act", "ive"])  # built at run time, like text from a request

print(status == "active")  # True
print(status is "active")  # False

== asks whether two objects have the same value. is asks whether two names share one object. Here status and the literal "active" are equal strings, but they are two separate objects, so is gives the wrong answer. Python prints a SyntaxWarning for the last line, because is with a literal is almost always a bug.

Use is for None and for sentinels

There is only one None object, so value is None is the standard check. A class can make == do something unusual, but it cannot change what is does.

is also suits a sentinel, an object you create only so that you can recognize it later. A sentinel tells "the key is missing" apart from "the key holds None":

MISSING = object()
settings: dict[str, object] = {"timeout": None}

print(settings.get("timeout", MISSING) is None)     # True, the key holds None
print(settings.get("retries", MISSING) is MISSING)  # True, there is no key

Create the sentinel once and reuse that one object. Each call to object() makes a new object, different from every other.

For strings, numbers and every other value, use ==. Python sometimes reuses one object for equal small integers or short strings, so is can pass in a quick test and fail on real input. The next article shows when that happens.

Choose which fields count for ==

@dataclass writes __eq__ for you. It compares the fields in the order you declare them. order=True also writes <, >, <= and >=, which compare the same fields in the same order. To leave a field out, declare it with field(compare=False):

from dataclasses import dataclass, field


@dataclass(frozen=True, order=True)
class Release:
    major: int
    minor: int
    patch: int
    source: str = field(default="", compare=False)


a = Release(1, 2, 0, "file")
b = Release(1, 2, 0, "network")
print(a == b, a is b)         # True False
print(len({a, b}))            # 1
print(a < Release(1, 10, 0))  # True, because (1, 2, 0) < (1, 10, 0)
print(a == object())          # False

source records where the release came from. It is not part of the release's value, so compare=False keeps it out of == and out of the ordering. Comparing a Release with an object of another class gives False.

Equal values must have equal hashes

A hash is an integer computed from a value. Sets and dictionaries use it to find a value without checking every item. hash(a) returns it. Because Release is frozen, the dataclass also writes __hash__, and it hashes the same fields that == compares. So compare=False keeps source out of the hash too.

A set looks a value up by its hash first, then checks ==. If two equal values had different hashes, the set would never compare them, and it would keep both. That is why equal values must have equal hashes.

The reverse is not required. Two different values can have the same hash. This is a collision, and the set checks == and keeps both values:

import sys

a = 0
b = sys.hash_info.modulus  # an int with the same hash as 0
print(hash(a) == hash(b), a == b)  # True False
print(len({a, b}))                 # 2

So the number of distinct hashes is not the number of distinct values.

Check your understanding status is "active" printed True in your REPL. Is the check safe to ship? Show answer

No. Python happened to reuse one string object in that test. Text read from a file or a request is usually a new object, and then the check returns False for a real "active" status. Write status == "active", which compares the characters and works whichever objects Python uses.

Check your understanding A frozen Version dataclass compares only major, minor and patch, but you write a __hash__ that also includes label. What goes wrong? Show answer

Version(2, 0, 0, "beta") and Version(2, 0, 0) are equal, but they usually get different hashes. A set then looks for them in different places, never compares them, and keeps both, so your count of distinct releases is too high. Hash exactly the fields that == compares, as the generated __hash__ does.

Going further

  • If you write __eq__ yourself, return NotImplemented for a type you do not handle. Python then asks the other object, and if that declines too, it falls back to is. That is why a == object() is False above.
  • mypy --strict reports == between types that can never be equal, such as bytes and str.
  • value is True is stricter than if value:. Use it only when 1 or a non-empty list must not count as true.
  • The data model reference gives the full rules for __eq__ and __hash__.

step 3 of 38

Memory, Copies and Slow Code

Up next Fix a Version Class That Compares Strings with is