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.
A Counter-Intuitive Start
Let us begin with a famous example:
1 | a = 256 |
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.
What id() Actually Means
According 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:
- As long as an object is alive, its
id()does not change. - Two different live objects cannot share the same
id().
In CPython (the most widely used implementation), this value is often the memory address of the object.
1 | s = "python" |
Warning
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.
Lifetime-Unique, Not Globally Unique
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.
1 | print(id(object())) |
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:
1 | a = object() |
Small Integer Cache
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.
1 | a = 256 |
For values outside the commonly cached range:
1 | x = 257 |
The key message is not the exact boundary. The key message is that identity behavior for literals can be affected by implementation-level reuse.
Constant Pooling and Compilation Context
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.
1 | import dis |
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:
- In a
.pyfile, both lines are often compiled together. - In REPL, each input is usually a separate compile unit.
So “same text” does not always mean “same compiled context.”
Note
When is changes across contexts, it often reflects constant reuse strategy, not value semantics. For value comparison, use ==.
String Interning
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.
1 | a = "hello" |
For strings with spaces or symbols, identity is less predictable:
1 | s1 = "hello world" |
You can force interning with sys.intern():
1 | import sys |
Practical use case: repeated short strings (CSV field names, status labels, protocol tokens) can consume less memory when interned carefully.
1 | import sys |
is vs ==: Practical Rules
The difference is fundamental:
iscompares identity (same object)==compares value (via__eq__)
1 | a = [1, 2] |
One strong rule in real projects: use is None, not == None.
1 | x = None |
Why is is None better?
Noneis a singleton, so identity is the correct semantic check.==calls__eq__, which may have custom behavior.
For example, with NumPy arrays:
1 | import numpy as np |
is None is both safer and clearer.
Mutable Objects and Identity Changes
id() is also a great tool to understand in-place mutation vs rebinding.
append mutates a list in place; + creates a new list:
1 | lst = [1, 2] |
+= has type-specific behavior:
1 | # list: usually in-place |
Classic trap:
1 | t = ([1, 2], [3, 4]) |
Why does it mutate even though it errors?
t[0]is a list, so in-place addition happens first.- Python then attempts to assign the result back.
- Tuple item assignment is illegal, so a
TypeErroris raised.
The mutation already happened before the exception.
Functions, Classes, and Modules Also Have id()
Everything in Python is an object, including functions, classes, and modules.
1 | import sys |
Modules often behave like singletons due to import caching in sys.modules:
1 | import math |
Practical Use and Real Pitfalls
Understanding identity is useful, but misuse is dangerous.
A bad pattern is using id() as a long-term tracking key:
1 | obj = object() |
This can create false matches in long-running programs.
A better approach is weakref:
1 | import weakref |
This tracks object relationships safely without relying on reusable integer identities.
Conclusion
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.
References
- Python Built-in Functions:
id()
https://docs.python.org/3/library/functions.html#id - CPython source (
Objects/longobject.c)
https://github.com/python/cpython/blob/main/Objects/longobject.c - CPython internal runtime constants
https://github.com/python/cpython/blob/main/Include/internal/pycore_runtime_structs.h - Python
sys.intern()
https://docs.python.org/3/library/sys.html#sys.intern - Python
weakref
https://docs.python.org/3/library/weakref.html