Python's Identity Crisis: The Peculiar Case of Integer Caching
Why is this Languages meme funny?
Level 1: One Toy or Two?
Imagine a teacher has a box of special toys numbered 1 up to 256. There’s only one toy for each number in that box. So if one child asks for toy number 256 and another child also asks for toy 256, the teacher gives them the same exact toy from the box. Later, the kids realize they literally had the same toy (one shared item). Now, if the kids ask for toy number 257, the teacher doesn’t have that one pre-made in the box. She makes a new toy 257 for the first child, and when another child asks for 257, she makes another separate toy 257 just for that child. Those two toys both have “257” written on them and look identical, but they are actually two different physical toys. Each kid is holding their own copy. So when they compare, they say, “these aren’t the same object, just two copies that look the same.”
The joke here is that Python behaves like that teacher. For certain small numbers, everyone gets one shared object (so it really is the exact same thing each time), and for bigger numbers, each request gets a brand-new object (so you end up with duplicates that are only superficially the same). It’s funny because you wouldn’t expect the rules to suddenly change after a particular number — that little surprise makes us laugh at how the computer handles things behind the scenes.
Level 2: Identity vs Equality
For a newer Python developer, the key lesson here is understanding identity versus equality. The operator == checks if two values are the same in value, but is checks if they are actually the same object. Think of each object in Python having a unique ID or address in memory. So a is b will only be True if a and b are literally references to the exact same thing internally.
In the meme’s code example:
a = 256
b = 256
print(a is b) # True
a = 257
b = 257
print(a is b) # False
The result is surprising at first. Why does 256 give True for a is b but 257 gives False? It turns out Python has a little optimization: it interns (reuses) some integers. Integers from -5 up to 256 are kept in a special cache inside the Python interpreter. So when you do a = 256 and b = 256, Python doesn’t create two separate 256 objects in memory. It gives both a and b a reference to the exact same pre-existing 256 object. They genuinely are the same object, hence a is b evaluates to True.
However, 257 is not in that pre-made cache. So a = 257 creates a new integer object for 257, and b = 257 creates another new integer object. Now a and b hold two different objects that happen to have the same numeric value. They look equal (a == b would be True because 257 equals 257), but they are not the very same object. That’s why a is b comes out False for 257 – each variable has its own 257 object.
The important takeaway is that in Python, you should use == to compare values in most cases, especially for numbers. The is operator is a special tool that checks identity, which is only useful when you actually want to know if two references are to the one single object (for example, checking if x is None: is correct, because there’s only one None object). The meme is a playful demonstration of what can happen if you mix these up. Python’s small integer caching is a behind-the-scenes feature to make number handling faster, but if you weren’t aware of it, seeing True then False like this can be pretty confusing!
Level 3: Ghosted by 257
From a seasoned Python developer’s perspective, this meme is poking fun at a classic gotcha: using the Python is operator where == should be used. The code snippet in the image looks innocent — just two variables holding the same number — yet it prints True for 256 and False for 257. Why the inconsistency? It’s highlighting Python’s small integer interning in a tongue-in-cheek way.
In Python, is checks object identity (think: are these the exact same object in memory?), while == checks value equality (do the objects have the same value?). Ideally, you wouldn’t use is to compare plain numbers at all, but here someone did, and the results are weird. For 256, Python’s small integer singleton cache kicks in, so a and b actually reference one shared object. It’s as if the number 256 is a single common instance. Thus a is b unexpectedly returns True, leading you to believe everything is fine. But for 257, Python creates two distinct int objects, so a is b comes back False — surprise! The second 257 is not the same object as the first one, essentially "ghosting" any assumption of identity.
This humor resonates with experienced devs because we’ve seen it in real code. It’s that sneaky language quirk where someone writes something like:
status_code = 200
if status_code is 200:
print("OK")
It works by accident (200 is interned, so it passes the is check), but if they later change the code to:
status_code = 500
if status_code is 500:
print("Error")
nothing prints – the check fails even though status_code == 500. That’s a head-scratcher if you don’t know about the interned ints! The meme’s text “What is wrong?” perfectly captures that perplexity. It’s the kind of bug that sneaks into codebases when a developer mistakenly uses is for integers or strings instead of ==. Seasoned Pythonistas know to compare numbers with == (and reserve is for special cases like checking against None or other singletons).
The punchline in the title, “Python’s interned ints: why 256 is ‘is’, but 257 ghosts you,” plays on the idea that Python promises to be simple and explicit, yet here it has a hidden optimization that can trip you up. It’s not a bug in Python itself – it’s a deliberate performance feature – but it can manifest as a bug in your code if you’re not careful. For a senior dev, this meme is both funny and a gentle reminder: even in a clean high-level language, under-the-hood details (like object caching) can lead to spooky surprises.
Level 4: Cache Miss at 257
Deep inside CPython (the standard Python interpreter) there’s a cunning performance trick at play. Python represents every integer as an object (PyLongObject in C), which normally means each time you create an integer, you allocate a new chunk of memory. However, creating and destroying tons of small numbers (like loop counters, array indices, etc.) would be inefficient. So CPython uses a small integer cache (a form of interning, inspired by the flyweight pattern): it pre-allocates all integers in the range -5 to 256 at startup and keeps them around for reuse.
This means whenever your code needs an integer in that range, Python doesn’t create a new object – it hands you a reference to an existing one. For example, when you do a = 256 and then b = 256, under the hood both a and b point to the exact same PyLongObject in that cache. They have the same memory address (you can verify with id(a) and id(b)), so a is b rightly returns True. The is operator in Python is essentially a pointer comparison: it checks if two references are to the same object in memory. Here, they are.
But if you assign a = 257 and b = 257, Python goes down the normal path: 257 is outside the cached range, so a brand new integer object is created for each assignment. Now a and b refer to two different objects that just happen to both represent the numeric value 257. Their id() values are different, so a is b comes out False. In other words, the second 257 doesn’t reuse the first one – there’s no interned object to grab – and the two 257s have distinct identities (the first 257 is left hanging, effectively ghosting you when you check identity).
The choice of caching -5 through 256 is an implementation detail of CPython (historically chosen to cover common small numbers; 256 is 2^8, a nice round binary boundary). It’s a trade-off: caching a wider range would consume more memory for diminishing speed returns. Other Python interpreters (PyPy, Jython, etc.) may intern differently, but this range has been a long-standing default. Importantly, this behavior is a CPython performance feature, not a guaranteed language rule. Code that relies on it can become confusing. The meme cleverly exploits this quirk: what seems like identical assignments produce different outcomes for is because of Python’s hidden caching mechanism. This deep dive into Python’s memory management explains exactly why 256 is special here – and why 257 behaves like a completely separate ghost each time.
Description
The image presents a programming quiz-style meme titled 'What is wrong?'. It shows two blocks of Python code with their respective outputs. The first block assigns the integer 256 to variables 'a' and 'b', and the expression 'a is b' evaluates to 'True'. The second block assigns 257 to 'a' and 'b', but this time 'a is b' evaluates to 'False'. Both blocks contain the watermark '#clcoding.com'. The technical context behind this is a CPython optimization where a range of small integers (from -5 to 256) are pre-allocated and cached. The 'is' operator checks for object identity (whether two variables point to the same memory location), not just value equality. For 256, both 'a' and 'b' point to the same cached object, so 'a is b' is True. For 257, which is outside this range, Python creates two separate objects in memory, making 'a is b' False. This behavior is a classic 'gotcha' that contradicts Python's reputation for being straightforward, as noted by the post's caption
Comments
113Comment deleted
The Zen of Python says 'There should be one-- and preferably only one --obvious way to do it.' CPython's integer cache then whispers, '...unless the number is bigger than 256.'
256 gets the VIP singleton pass in CPython’s small-int pool; 257 shows up and your ‘is’ test fails harder than your promised five-nines SLA
After 15 years of Python, you still explain to juniors that 'is' checks if two variables are the same person at a party, while '==' checks if they're wearing the same outfit. Then CPython's integer caching from -5 to 256 shows up like that one friend who insists on being a singleton at every gathering
Ah yes, the classic 'is' vs '==' interview trap that separates those who've read CPython source from those who just write Python. Nothing says 'senior engineer' quite like knowing that -5 to 256 live in a special VIP lounge while 257 has to wait outside like a peasant. It's Python's way of teaching us that identity is just a social construct... until you hit production and wonder why your cache key comparison works in dev but fails with real data. Pro tip: if your code's correctness depends on whether integers are the same object in memory, you're not writing Python - you're writing a very expensive philosophy dissertation on the nature of sameness
If your dedup logic uses 'a is b', congratulations - your correctness now depends on CPython’s small-int cache; your SLA is fine until a customer orders item 257
Python's small int cache: the singleton pattern 256 lives for, but 257 has to rent its own object - talk about class discrimination
Relying on CPython’s small‑int cache for correctness is how ‘is’ becomes a portability bug - works on your laptop, detonates on PyPy
wtf? Comment deleted
maybe 256 is byte and integer size of a byte is stored as value? Comment deleted
No, byte max value is 255 Comment deleted
then i have no clue Comment deleted
numbers from some negative number to 256 are singletons, for performance reasons. This is literally UB too, since this is only an implementation peculiarity in CPython. Comment deleted
i think it has to do with the id of values Comment deleted
Caching, I guess. Numbers up to 256 are cached and returned as a single data type. 257 is not cached, so it is two data types in memory. At least that's how it works in Java Comment deleted
yes Comment deleted
can you gives us the whole story pls ? Comment deleted
https://www.codementor.io/@arpitbhayani/python-caches-integers-16jih595jk I guess this one should help Comment deleted
thanks Comment deleted
This is true Comment deleted
This is on the same level as JavaScript fuckery Comment deleted
Not even slightly, JS is another level Comment deleted
I'd say this one is more obscure than JS magicks. All the crazy stuff happening in JS is well documented at least. Comment deleted
JavaScript's pretty simple, until you mix numbers, objects and arrays with ToString(). Who even thought that was a good idea? Comment deleted
and valueOf Comment deleted
who even thought using JavaScript is a good idea? Comment deleted
i -= -1...) Comment deleted
What? -=-1 is equal to +=1 Comment deleted
what about types Comment deleted
There is TS to fix it. Comment deleted
Also: char * s = "12"; s-=-1; printf("%s", s); Comment deleted
2?? Comment deleted
> checks for reference equality rather than value equality in a built-in datatype that isn't meant to be used like an object > wonders why UB is happening no, this isn't js-like fuckery Comment deleted
This is not UB though Comment deleted
from 0 to i guess 256 each number has it's own id Comment deleted
also not from 0 Comment deleted
but from -5 Comment deleted
thanks for the correction Comment deleted
it also sounds so funny Comment deleted
is is used in comparisons to check whether the variables refer to the same address in memory. As the numbers up to 256 are cached in memory, it produces this counterintuitive result. Comment deleted
"We cache -4" Comment deleted
the only reason you want to use is operator is when you want to check variable for nullability (if x is None:) I dunno is there any other scenario when you really need to test variables by their memory references Comment deleted
type(X) is Y Comment deleted
if isinstance(X, Y): is better in most cases, because it works for subclasses too Comment deleted
And multiple types at once (second argument can be tuple of types). Comment deleted
I also used x is False to avoid coercion of 0 and "" to False Comment deleted
https://ideone.com/EgC18t Comment deleted
maybe that's not CPython. It is UB after all Comment deleted
My opinion may be biased due to me working with that thing on a daily basis and having gotten used to the black magic. Comment deleted
Just don't use "is", You will almost never face this behavior in the code, in contrary to JS weirds Comment deleted
What exactly does "is" do then? Apparently it's more than comparison by value? Comment deleted
It's comparison of address in memory. 'id(a) == id(b)' Though objects may be equal, but if in memory they are different you get false. If you want to compare objects you use "==" Comment deleted
Thanks for letting me know Comment deleted
Similar thing happens for other objects as well, for example for the string. Take the quiz: https://github.com/zshimanchik/python_core_tutorial/blob/master/02_int_caching.ipynb Comment deleted
bro: literally doesn't understand what "is" does also bro: wtf i don't understand what "is" does 🤯 omg so unintuitive Comment deleted
Um? Comment deleted
Wouldn't be surprised if this were both configuration and implementation specific. Essentially the "meme" is a nice example for why you should know operators and types and such and pick the right ones ... (I guess in all languages) Comment deleted
print python version. reproduced on 3.11.2 Comment deleted
I checked, it depends on how you call it. https://t.me/devs_chat/98827 Comment deleted
i.e. in a file a is b → True and inside the interactive shell, a is b → False Comment deleted
if repl than yes, otherwise it's done bugless i assume, yes Comment deleted
wdym bugless, all of this is intended behaviour Comment deleted
*me sitting here for two straight minutes, trying to figure out wheres REPL part came from* Comment deleted
you have three options - 1. write to a file and exec it, 2. run REPL and put them there or 3. pass some code as argument to the interpreter Comment deleted
ik, just had a lil brain melt Comment deleted
"caching numbers" makes me vomit Comment deleted
Ok, I see Comment deleted
Honestly, you're pretty damn lucky if you've only encountered this symptom in Python, not due to pointer provenance Comment deleted
"is" checking if it is the same object... Up to 256 ints are just a pointer, so it is literally points at the same memory cell... Comment deleted
all ints are objects in python, but they're only singletons up until 256 (plus some negative numbers) Comment deleted
Like writing to a log file with rotation without losing the output? Comment deleted
Mine is too a bit. Since JS itself wasn't meant to be working with filesystems, NodeJS and other runtimes followed that by not supporting syscalls which track FS nodes. Comment deleted
don't compare numbers with is >:I Comment deleted
maybe because usually such code is written only by some software engineering babies or fools Comment deleted
Wait till you realize that JavaScript uses 64 bit integers for everything, pointers, consts (like null, true, false, NaN, etc) and that objects are sometimes stored like vectors, sometimes as a fucking "butterfly" which is custom type that is a 32 bit pointer stored in a 64 bit integer and points in the middle of the array. The left side of the array stores values/pointers to the properties, and the right side stores array items if the object is also an array (which it is sometimes) Comment deleted
stop it you are scaring me Comment deleted
That was not all Comment deleted
oki imma leave now :) Comment deleted
The problem is that you mixed tabs with spaces Comment deleted
floating point numbers range from 0001 0000 0000 0000x to FFFE FFFF FFFF FFFFx Comment deleted
what Comment deleted
Wait wait isn't every bit sequence a valid fp number? Comment deleted
I'm not sure about the trick described above, but there are many different NaN values which can be abused to store extra data for NaN Comment deleted
where is it from Comment deleted
hmm Comment deleted
still UB, it's not the job of python to make sure you don't do cursed shit with it Comment deleted
Python exists because it babysits programmers. It's slow, hungry, and lacks power. And now you claim that it fails even in its sole purpose? Comment deleted
python exists to be quick to code, not to babysit programmers. Comment deleted
Do you think Python is quick? To me, it feels like it took the worst of both worlds: the clumsiness of compiled languages and the sluggishness of interpreted ones. Just compare it to Perl, for example, and you'll see how to achieve the same results with half the code. But sure, if by "quick" you mean "pull it from a library and pray," then you're correct... however from this perspective Bash even more "quick". Comment deleted
it's not clumsy at all though bash is cursed Comment deleted
bash is the OG python Comment deleted
and no, I barely ever use external libs for my python scripts Comment deleted
Bash isn't as universal though Comment deleted
in this case, it's because the parser somtimes likes to make two equivalent literals the same object, to save on instanciating overhead Comment deleted
so it actually compiles to bytecode that looks something like _hidden = 257 a = _hidden b = _hidden Comment deleted
256 is still part of the numeric range where python uses singletons Comment deleted
all I know is that baba is you Comment deleted
change your python Comment deleted
What python is that? I tried it on python 3.10 and got the described result Comment deleted
i tried on 3.5 3.6 3.8 and 3.9 Comment deleted
and 3.11 Comment deleted
try writing it to a file and executing it then, the precompile stage sometimes does a bit of optimization the interactive interpreter can't Comment deleted
lol > This is guaranteed to be unique among simultaneously existing objects. such a scam Comment deleted
ints aren't meant to be used like regular objects Comment deleted
but that makes me wonder why its only 256 values Comment deleted
because ints up to 256 are singletons (in CPython at least) Comment deleted
i mean why not regular int but only char sized? everything bigger than byte eventually degrades to PyNumber? Comment deleted
all python ints are objects. The interpreter just saves some time on creating lower ones, because they get used a lot in e.g. iterators and such Comment deleted
I thought they start to become one after int32 boundaries passed. But I guess its only int16 Comment deleted
no, all of em are objects in python. They become slightly different objects at some point though, when values get too large (or small) they keep track of the value via a BigInt-ish structure internally. Comment deleted
(iirc, afaik) Comment deleted
I know that they become big int at some point (with enormous base as representation) Comment deleted
it's an arbitrary decision. They could've made it 100 or 123. They still probably chose 256 because 2**8 is a nice number in computing Comment deleted
ok I hate js as much as the next guy (or gal or nb pal) but if you modify your object inside its valueOf, that's your fault. Comment deleted