a = 256
b = 256
print(a is b) # Expected: True
x = 257
y = 257
print(x is y) # Expected: may be True or FalseTrue
False
id()Anthony
April 14, 2026
At first glance, id() looks like a tiny utility: it just returns an integer. But once you start using it to inspect real code, you quickly realize something deeper: many bugs and “weird” behaviors come from misunderstanding what a Python variable actually is.
This article uses runnable examples to explain one core idea: in Python, variables are names, and objects are the real entities. Along the way, we will cover small integer caching, constant pooling, string interning, is vs ==, and a few classic pitfalls that appear in production code.
Let us begin with a famous example:
True
False
Many developers see this and think Python is inconsistent. It is not. The behavior is usually consistent within a given implementation and execution context, but the result can vary across contexts (script vs REPL) and across implementations.
To understand why, we first need to be precise about what id() means.
id() Actually MeansAccording to the official Python docs, id(object) returns an integer that is unique and constant during the object’s lifetime.
That statement has two important parts:
id() does not change.id().In CPython (the most widely used implementation), this value is often the memory address of the object.
4368988176
True
In CPython, id() is often the memory address, but this is an implementation detail, not a language-level guarantee. PyPy, Jython, and other implementations may use different strategies.
So id() is useful for debugging and learning internals, but you should not rely on CPython-specific behavior as business logic.
A common misunderstanding is: “If id() is unique, it should never repeat.” That is incorrect.
The docs say unique during the object’s lifetime. After an object is destroyed, its old id() may be reused.
4500198144
4500198400
4500198144
These temporary objects are created and discarded immediately, so CPython may reuse the same memory slot.
If you want to compare identities safely, keep references alive:
Now back to 256 vs 257.
CPython pre-allocates a range of small integer objects and reuses them. In many tutorials you will see -5..256 as the typical range. In newer CPython branches, related constants are internal and version-dependent, so avoid treating one exact range as a language guarantee.
True
True
For values outside the commonly cached range:
The key message is not the exact boundary. The key message is that identity behavior for literals can be affected by implementation-level reuse.
If 257 is not always from the small-int cache, why is x is y sometimes True anyway?
Because equal literals in the same code object may be loaded from the same constant slot.
3 0 RESUME 0
4 2 LOAD_CONST 1 (257)
4 STORE_FAST 0 (a)
5 6 LOAD_CONST 1 (257)
8 STORE_FAST 1 (b)
6 10 LOAD_FAST 0 (a)
12 LOAD_FAST 1 (b)
14 IS_OP 0
16 RETURN_VALUE
You may see both assignments load the same constant index (for example, LOAD_CONST 1). That means both names refer to the same constant object in that compiled unit.
This is also why script files and REPL input can differ:
.py file, both lines are often compiled together.So “same text” does not always mean “same compiled context.”
When is changes across contexts, it often reflects constant reuse strategy, not value semantics. For value comparison, use ==.
Strings can also be reused through string interning.
A simple way to think about it: if two strings are identical, Python may keep one shared copy in memory.
For strings with spaces or symbols, identity is less predictable:
You can force interning with sys.intern():
True
Practical use case: repeated short strings (CSV field names, status labels, protocol tokens) can consume less memory when interned carefully.
is vs ==: Practical RulesThe difference is fundamental:
is compares identity (same object)== compares value (via __eq__)One strong rule in real projects: use is None, not == None.
True
True
Why is is None better?
None is a singleton, so identity is the correct semantic check.== calls __eq__, which may have custom behavior.For example, with NumPy arrays:
[False False False]
False
is None is both safer and clearer.
id() is also a great tool to understand in-place mutation vs rebinding.
append mutates a list in place; + creates a new list:
True
False
+= has type-specific behavior:
True
False
Classic trap:
TypeError
([1, 2, 5], [3, 4])
Why does it mutate even though it errors?
t[0] is a list, so in-place addition happens first.TypeError is raised.The mutation already happened before the exception.
id()Everything in Python is an object, including functions, classes, and modules.
4501764864
36446986256
4368951200
Modules often behave like singletons due to import caching in sys.modules:
Understanding identity is useful, but misuse is dangerous.
A bad pattern is using id() as a long-term tracking key:
This can create false matches in long-running programs.
A better approach is weakref:
1
This tracks object relationships safely without relying on reusable integer identities.
id() helps you see Python’s object model in action: variables are names, objects hold state and behavior.
Once you internalize that model, many confusing behaviors become predictable: is vs ==, mutable vs immutable operations, interning, constant pooling, and identity reuse.
At the same time, keep this boundary clear: many examples here are CPython implementation details. They are excellent for debugging and performance tuning, but they should not become hard assumptions in application logic.
In the next article, we will go deeper into mutable vs immutable objects, especially how they affect function arguments, default values, and side effects.
id() https://docs.python.org/3/library/functions.html#idObjects/longobject.c) https://github.com/python/cpython/blob/main/Objects/longobject.csys.intern() https://docs.python.org/3/library/sys.html#sys.internweakref https://docs.python.org/3/library/weakref.html