Unexpected behaviour of `is` operator towards two identical objects!

Python 3.14.0a0 (heads/main:b02301fa5a5, Sep 14 2024, 10:15:21) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def f(x, y):
...     return x + y
...
>>> f((1,2),(2,)) is f((1,2),(2,))
False
>>> f(2,3) is 5
<python-input-2>:1: SyntaxWarning: "is" with 'int' literal. Did you mean "=="?
  f(2,3) is 5
True

Why should not two hashable object with the same value be equal when they are passed through a function?

is doesn’t check for equality. == does. Python told you this much in the code example you’re pasting.

3 Likes

They are equal but not necessarily identical (ie the same object).
Having them being identical is an implementation detail and you shouldn’t rely on that (as the warning says).

3 Likes

tl;dr is checks identity. == checks equality. If a is b then a == b but the reverse does not always hold.

>>> a = [1, 2]
>>> b = [1, 2]
>>> a == b
True
>>> a is b
False
>>> c = a
>>> c is a
True

The long version:
Python variables are references to objects, not the objects themselves. is looks at the reference to determine if they are the same. == looks at the values being referenced to determine if they are equal.
@trey did a very nice video on this.

1 Like

Neither direction holds:

>>> a = b = float('nan')
>>> a is b, a == b
(True, False)

Python assumes for a few cases that this is true (e.g. tuple compare), but it’s not actually the case.

2 Likes

Careful with that implication. The type of a might define == to give False with itself.

An example is float('nan').

TIL something new.

It’s not quite that Python assumes that identity implies equality, but that for those sorts of comparisons, the actual check is “identical or equal”.

No. Quote from the language reference:

Sequences compare lexicographically using comparison of corresponding elements. The built-in containers typically assume identical objects are equal to themselves.

5 Likes

The is operators tests if object are identical. Per docs: The operators is and is not test for an object’s identity: x is y is true if and only if x and y are the same object. An Object’s identity is determined using the id() function. x is not y yields the inverse truth value.

It’s easy to see, that in you case objects aren’t identical:

>>> (1, 2) + (2,)
(1, 2, 2)
>>> id(_)
139961654850496
>>> (1, 2) + (2,)
(1, 2, 2)
>>> id(_)
139961654839424

Funny I find it easier to think of it as ‘same pointer’ vs ‘same value’.
Regardless, it’s useful to understand under which circumstances you can expect a is b and which operations change the underlying value of a rather than constructing a new value, because such operations would also change the value of other variables with the same pointer.

Be careful with that as the validity of id return value depends on the object’s lifetime: two different objects can share a same id if they do not exist in memory at the same time, but they are not identical.

>>> [1, 2, 3]
[1, 2, 3]
>>> id(_)
137700610591168
>>> [4, 5, 6]
[4, 5, 6]
>>> id(_)
137700610591168

Yes, but you can’t compare such objects with is operator:)

Id’s aren’t determined by object’s content. Same (wrt equality) objects can have different id (in CPython is’t just address of the object in memory) within one session:

>>> [1, 2, 3]  # (1)
[1, 2, 3]
>>> id(_)
140433683777216
>>> [1, 2, 3]  # (2); at this point (1) might be even garbage-collected
[1, 2, 3]
>>> id(_)
140433681649600
>>> [1, 2, 3] is [1, 2, 3]
False

Yes it was just to remind that id comparison is not enough to consider objects are the same, because of that lifetimes.

One could think that typing [1, 2, 3] is [1, 2, 3] is the same as id([1, 2, 3]) == id([1, 2, 3]) for example.

>>> id([1, 2, 3]) == id([1, 2, 3])
True