Python never make copies unless explicitly asked to (for example, slicing a list does ask Python to copy that part of the list, shallowly).
"Does add_three refer to same int object that n is pointing to?" -- yes, only references to that int are being passed around and held in frames. In this case this applies whatever the value of n.
Any Python implementation is allowed to keep a single copy, or multiple copies, of immutable objects line ints -- whatever's most convenient to that implementation, given the semantics are not affected anyway.
So in a given implementation it could happen that every mention of literal 3 refers to the same int object but mentions of literal 333 need not. E.g:
2>>> a=333; b=333; print(id(a), id(b))
(4298804944, 4298804944)
2>>> a=333
2>>> b=333
2>>> print(id(a), id(b))
(4298753600, 4298753336)
The semantics of the two cases are absolutely identical; in the first case the compiler (intrinsically called on the whole line at once) finds it handy to instantiate and use a single int worth 333, in the second case it prefers to make and use two such instances -- either is completely fine, given int's immutability (same goes for other number types, strings, tuples, frozen sets -- but not for mutable types).
Note that when the Python specification refers to "same semantics", it explicitly includes introspection, which may be able to pinpoint implementation differences between semantically equivalent states.
id (normally returning the memory address of an object, in current popular implementations of Python, but in any case an id that's unique per object as long as the object lives, per language specs) is introspection, as consequently is the is operator. So you can if you wish use it to understand some optimizations a given implementation may perform, or not.
So on to your other Qs: "Is my understanding correct?" -- no.
"Why this difference" -- def builds a function object, which is mutable, so any def even with identical function definitions must return a new object, just like e.g [] builds a list object, mutable, so any [] must return a new object. 3 build an int object, which is immutable, so any 3 is allowed (per language rules) to return either the same or a new object.
One more question was added in an edit: "What is the idea for stack frame being alive when function type object is returned?"
Answer: every object stays alive as long as it's reachable. An outer function's frame, in particular, stays alive as long as inner (nested) functions is returned, if they refer to names in the outer frame.
(Any Python implementation doesn't have to garbage-collect objects that don't any more need to be alive -- it may delay that garbage collection as long as it pleases, or can perform it at once -- implementation details!-).
nthrough, what do you expect to happen?add_threerefer to sameintobject thatnis pointing to? This is where I am asking whether the return mechanism is by copy? For simplicity, assume the value is > 256.n *= 1the returned object would be different, as that changed the referencen. This is where the small integer optimisation is important, sinceid(n * 1) == id(n)iffnis a small integer.