The Python cookbook is correct. You have exclusive access to the Python interpreter at the time PyGILState_Ensure() returns. Exceptional access means that you can safely call all the functions of CPython. And that means the current C thread is also the current active Python thread. If the current C thread did not have a corresponding Python thread before, PyGILState_Ensure() will automatically create it for you.
This state is after PyGILState_Ensure() . And you also have a GIL acquired at that moment.
However, when calling other CPython functions, such as PyEval_EvalCode() or any other, they can implicitly make the GIL be released meanwhile. For example, this is so if the Python time.sleep(0.1) operator is implicitly receiving a call somewhere. Although the GIL is freed from this topic, other Python threads may be running.
You have only a guarantee that when PyEval_EvalCode() (or any other CPython function that you called) returns, you will again have the same state as before, that is, you are on the same active Python thread, and you have GIL again.
About your original question: there is currently no way to achieve this, that is, invoke Python code and avoid the GIL being released as a result somewhere in between. And this is good, otherwise you can easily end up in dead ends, for example. unless you allow any other thread to release some kind of lock that is currently being held.
About how to implement your use case: the only real way to do this is in C. You would PyGILState_Ensure() to get the GIL. And at this point you should only call those CPython functions that cannot have the side effect of calling other Python code. Be very careful. Even PyObj_DecRef() can call __del__ . It would be best to avoid calling the CPython functions and manually moving the CPython objects. Note that you probably don't need to do this as hard as you have outlined: there is a basic CPython memory allocator, and I think you can just get the information from there.
Read here about memory management in CPython.
The related code is in pymem.h , obmalloc.c and pyarena.c . See the _PyObject_DebugMallocStats() Function, although it cannot be compiled into your CPython.
There is also a tracemalloc module , which, however, will add some overhead. Perhaps its basic C code ( _tracemalloc.c file) is useful, however, to understand the insides a bit better.
About sys.setswitchinterval(1000) : This is only for passing Python bytecode and processing it. This is basically the main CPython loop in PyEval_EvalFrameEx in the ceval.c file. There you will find the following part:
if (_Py_atomic_load_relaxed(&gil_drop_request)) ...
All logic with a switching interval is considered in the file ceval_gil.h .
Setting the maximum switching interval means that the main loop in PyEval_EvalFrameEx will not be interrupted for a longer time. This does not mean that there are no other features that the GIL could release in the meantime, and another thread could work.
PyEval_EvalFrameEx will execute Python bytecode. Suppose this calls time.sleep(1) . This will call the inline implementation of the C function. You will find this in time_sleep() in the timemodule.c file. If you follow this code, you will find the following:
Py_BEGIN_ALLOW_THREADS err = select(0, (fd_set *)0, (fd_set *)0, (fd_set *)0, &timeout); Py_END_ALLOW_THREADS
Thus, GIL is being released in the meantime. Now, any other thread waiting for a GIL can pick it up and run other Python code.
Theoretically, you might think that if you set a high switching interval and you never call any Python code, which in turn can issue a GIL at some point, you will be safe. Please note that this is almost impossible. For example. GC will be called from time to time, and any __del__ some objects may have various side effects.