Run it
The cache boundary, the folding that hides it, and the nan case where equality and identity openly disagree.
The cache boundary - and the constant folding that hides it.
For anything mutable the two never agree unless you aliased on purpose.
And the case where the two operators openly disagree.
Two different questions
a == b calls a.__eq__(b), which a type defines however it likes. a is b compares the addresses of the two objects and cannot be overridden by anything.
So == is a question about values and is is a question about storage. Confusing them usually works, which is the problem: the code passes its tests on small inputs and fails on a value that happens to fall outside a cache.
The small-integer cache, and the folding on top of it
CPython pre-creates every integer from −5 to 256 at startup and hands out the same object whenever one is needed. So int('256') is int('256') is True and int('257') is int('257') is False.
There is a second, separate mechanism that makes this hard to demonstrate. Constants written in one code block are folded by the compiler, so e = 257; f = 257 puts one constant in the code object and e is f is True — nothing to do with the cache, and it is why the same experiment gives different answers in a script and at a REPL prompt.
Short strings that look like identifiers get the same treatment through interning. The lesson is not the boundaries; it is that these are decisions an interpreter is free to change.
Where is belongs
Three cases, and in effect nothing else.
Singletons. x is None, x is True, x is False. There is exactly one of each, so identity is the correct test and it is faster than == besides.
Sentinels. _MISSING = object(), then if arg is _MISSING. The whole point of the object is that nothing else can be it.
Genuine aliasing questions. "Are these two names the same list?" is an identity question, and it is what you want when checking whether a caller handed you the object you already hold.
The follow-up: == that disagrees with itself
"Can x == x be False?" Yes. float('nan') == float('nan') is False by IEEE rule, and so is nan == nan for the same object — while nan is nan is True. That is the cleanest possible demonstration that the two operators are answering different questions, and it is why x in [float('nan')] can be True: the container check tries is first as a shortcut.
Edge cases to raise
Volunteering these is most of what separates a correct answer from a good one.
The cache boundaries are not a language guarantee. PyPy, MicroPython and a future CPython are free to differ, so any behaviour that depends on them is a bug that happens to pass today.
x is not None reads better than not x is None and is the same operator. Worth using, because the second form invites a misparse.
Do not use is to test for emptiness or truth. if x is True fails for 1, for a non-empty list, and for anything with __bool__ - which is almost certainly not what you meant.