Identity versus equality
== is value equality: it calls type(a).eq(a, b) (with reflection and NotImplemented fallback rules), so the class decides what 'equal' means. is is identity: it checks whether both operands are the very same object, which in CPython means comparing addresses, and it cannot be overridden. Equal objects are not necessarily identical, but identical objects are almost always equal (NaN is the famous exception).
When I use each: is for singletons (None, True, False, Ellipsis) and sentinels, == for everything else. 'is None' is preferred because it is faster, expresses intent, and cannot be fooled by a custom eq; with numpy or pandas, x == None produces an element-wise array (or an ambiguous-truth-value error) while x is None answers the question you meant.
Common mistakes: using is to compare strings or numbers (it works by accident for small ints and interned strings in CPython, then fails elsewhere - Python 3.8+ emits a SyntaxWarning for is with a literal), and forgetting that collections shortcut with identity first, which is why a list containing NaN can compare equal to itself. Also remember that if you define eq without hash, instances become unhashable; equality and hashing must stay consistent.
0-2 years experience
2-5 years experience
5-8 years experience
8+ years experience