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
Hide 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
Hide 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, returnNotImplementedfor a type you do not handle. Python then asks the other object, and if that declines too, it falls back tois. That is whya == object()isFalseabove. mypy --strictreports==between types that can never be equal, such asbytesandstr.value is Trueis stricter thanif value:. Use it only when1or a non-empty list must not count as true.- The data model reference
gives the full rules for
__eq__and__hash__.