Objects are created as needed, in different places.
To start, when you write
b = 27
two things happen. The 27 expression is evaluated, resulting in an integer object being pushed onto the stack, and then, as a separate step, the object is assigned to b. Assignment doesn't create objects.
If you did just this:
27
The 27 expression is still evaluated. The object would be created*, then destroyed again as the reference count drops back to 0 again.
That's needed because you could pass that object to another function:
id(27)
needs something to be passed to the id() function. So 27 is added to the stack so you can call the function.
I'll use a mutable object instead of an integer, to illustrate that a new object is created; so instead of id(27) I'll use id([]) and ask the dis module to show me the bytecode that Python would execute:
>>> import dis
>>> dis.dis(compile('id([])', '', 'exec'))
1 0 LOAD_NAME 0 (id)
2 BUILD_LIST 0
4 CALL_FUNCTION 1
6 POP_TOP
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
The BUILD_LIST 0 opcode is used to create the empty list object and push it onto the stack, and CALL_FUNCTION 1 then calls id to passing in one value from the stack, which is that list.
I didn't use id(27) because immutable objects like integers and tuples and such are actually cached with the bytecode that is compiled; these are created when Python compiles the code (or when you load the .pyc bytecode cache from disk):
>>> dis.dis(compile('id(27)', '', 'exec'))
1 0 LOAD_NAME 0 (id)
2 LOAD_CONST 0 (27)
4 CALL_FUNCTION 1
6 POP_TOP
8 LOAD_CONST 1 (None)
10 RETURN_VALUE
Note the LOAD_CONST, it loads the data from the co_consts structure:
>>> compile('id(27)', '', 'exec').co_consts
(27, None)
So objects can be created when compiling, or when execuning special opcodes for specific Python syntax.
There are more places:
- There are more opcodes, for creating lists, tuples, dictionaries, sets and strings, for example.
- When you create an instance of a class,
type.__new__ will create an instance object on the heap. So CustomClass(arg1, arg2) creates an object with the right type.
- The same applies to all built-in types;
int(somevalue) creates an integer object on the heap.
- Plenty of built-in functions will create new objects as needed, returning those from calls
class, def statements and the lambda expression create objects (class objects, functions, and more functions, these are all objects too).
* Small integers are actually interned; for performance reasons, CPython keeps a single copy each of the integers between -5 and and 256, so these objects are actually created only once, and referenced everywhere you need one. See "is" operator behaves unexpectedly with integers. For the purposes of this answer I'm ignoring this.
And because they are interned, the result of 20 + 3 returns that single copy and the id() will still be the same as if you asked for id(23) directly.
There are more implementation details; there are many more. Some string objects are interned (see my answer here). Code evaluated in the interactive interpreter is compiled one top-level block at a time, but in a script compilation is done per scope instead. Because constants are attached to compiled code objects, that means that there are differences as to when constants are shared. Etc. etc.
The only objects you can rely on not being recreated all the time are explicitly documented in the datamodel documentation as being singletons; None being the most prominent of these.