Many thanks to @eryksun for the corrections!
This is because of a mechanism call interning
in Python:
Enter string in the table of “interned” strings and return the
interned string – which is string itself or a copy. Interning strings
is useful to gain a little performance on dictionary lookup – if the
keys in a dictionary are interned, and the lookup key is interned, the
key comparisons (after hashing) can be done by a pointer compare
instead of a string compare. Normally, the names used in Python
programs are automatically interned, and the dictionaries used to hold
module, class or instance attributes have interned keys.
Changed in version 2.3: Interned strings are not immortal (like they
used to be in Python 2.2 and before); you must keep a reference to the
return value of intern() around to benefit from it.
CPython will automatically intern short certain strings (1 letter strings, keywords, strings without spaces that have been assigned) to increase lookup speed and comparison speed: eg., 'dog' is 'dog'
will be a pointer comparison instead of a full string comparison. However, automatic interning for all (longer) strings requires a lot more memory which is not always feasible, and thus they may not share the same identity which makes the results of id()
different, for eg.,:
# different id when not assigned
In [146]: id('dog')
Out[146]: 4380547672
In [147]: id('dog')
Out[147]: 4380547552
# if assigned, the strings will be interned (though depends on implementation)
In [148]: a = 'dog'
In [149]: b = 'dog'
In [150]: id(a)
Out[150]: 4380547352
In [151]: id(b)
Out[151]: 4380547352
In [152]: a is b
Out[152]: True
For integers, at least on my machine, CPython will automatically intern up to 256 automatically:
In [18]: id(256)
Out[18]: 140511109257408
In [19]: id(256)
Out[19]: 140511109257408
In [20]: id(257)
Out[20]: 140511112156576
In [21]: id(257)
Out[21]: 140511110188504
UPDATE thanks to @eryksun: in this case the string 'a string'
is not interned because CPython only interns strings without spaces, not because of the length as I instantly assumed: for eg., ASCII letters, digits, and underscore.
For more details, you can also refer to Alex Martelli's answer here.