I spent a long time using 0 and "" as placeholders — if a variable held zero, I figured nothing had happened yet. Then I wrote a search loop that remembers the position of the first error in a log, and I couldn't tell "error on line 1" from "no error at all". None is Python's value for exactly that situation: a real value that means nothing yet.
first_error = None # nothing found yet
for i, line in enumerate(log_lines):
if "ERROR" in line:
first_error = i
break
if first_error is None:
print("clean run")
else:
print("first error on line", first_error + 1)
None is not zero and not an empty string — both of those are real values that can occur honestly. None means this hasn't been set yet, and nothing in your data can imitate it by accident.
a = [1, 2, 3]
b = [1, 2, 3]
a == b # True — same contents
a is b # False — two separate lists that happen to match
Two identical shopping lists are still two pieces of paper. == compares what's written on them; is asks whether they're the same sheet of paper. (One wrinkle: Python caches small integers and some short strings, so is can say True where you expected False. That's a reason to keep is for None and not stretch it further.)
None is a singleton — there is exactly one None in a running program — so identity is the precise question, and it can't be fooled:
if first_error is None:
print("clean run")
is can't be customized; it always asks the same literal question. == can be: a class defines its own __eq__, and I once used a library whose objects cheerfully reported themselves equal to anything you asked, None included. That was the afternoon my if x == None checks started lying to me. is None means the same thing in every Python file ever written; == None means whatever some class decided it means.
The other place None quietly does hero work is function defaults. This looked fine to me for months:
def add_item(item, cart=[]):
cart.append(item)
return cart
print(add_item("apple")) # ['apple']
print(add_item("pear")) # ['apple', 'pear'] — where did apple come from?
Default values are created once, when the function is defined — not on every call. So every call that omits cart shares the same list. The standard fix is the None pattern itself:
def add_item(item, cart=None):
if cart is None:
cart = []
cart.append(item)
return cart
cart=None means "nothing handed in yet", and the function builds its own list when it hears that. Notice which check the fix uses: is None, because identity is the one question None always answers truthfully.
So the short story: == for values, is for identity, and is None whenever the question is "has this happened yet?". None isn't a hole in your data — it's an honest value for the space before the data. Once I stopped fearing it, half of my flag variables and sentinel hacks disappeared.