Search code examples
pythonreferenceidentityequalitymutable

Is the "is" Python operator reliable to test reference equality of mutable objects?


I know the is operator in Python has an unexpected behavior on immutable objects like integers and strings. See "is" operator behaves unexpectedly with integers

>>> a = 0
>>> b = 0
>>> a is b
True        # Unexpected, we assigned b independently from a

When it comes to mutable objects, are we guaranteed that two variables expected (as written in the code) to reference two distinct objects (with equal value), will not be internally bound to the same object ? (Until we mutate one of the two variables, then of course the references will differ.)

>>> a = [0]
>>> b = [0]
>>> a is b
# Is False guaranteed ?

Put in other words, if somewhere x is y returns True (x and y being mutable objects), are we guaranteed that mutating x will mutate y as well ?


Solution

  • So long as you think some "is" behavior is "unexpected", your mental model falls short of reality ;-)

    Your question is really about when Python guarantees to create a new object. And when it doesn't. For mutable objects, yes, a constructor (including a literal) yielding a mutable object always creates a new object. That's why:

    >>> a = [0]
    >>> b = [0]
    >>> a is b
    

    is always False. Python could have said that it's undefined whether each instance of [0] creates a new object, but it doesn't: it guarantees each instance always creates a new object. is behavior is a consequence of that, not a driver of that.

    Similarly,

    >>> a = set()
    >>> b = set()
    >>> a is b
    False
    

    is also guaranteed. Because set() returns a mutable object, it always guarantees to create a new such object.

    But for immutable objects, it's not defined. For example, the result of this is not defined:

    >>> a = frozenset()
    >>> b = frozenset()
    >>> a is b
    

    frozenset() - like integer literals - returns an immutable object, and it's up to the implementation whether to return a new object or reuse an existing one. In this specific example, a is b is True, because the implementation du jour happens to reuse an empty frozenset. But, e.g., it just so happens that

    >>> a = frozenset([3])
    >>> b = frozenset([3])
    >>> a is b
    False
    

    today. It could just as well return True tomorrow (although that's unlikely - while an empty frozenset is an easy-to-detect special case, it would be expensive to ensure uniqueness across all frozenset objects).