Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CPython: Understanding the difference between tp_dealloc and tp_finalize

Tags:

python

After reading the documentation for both tp_dealloc and tp_finalize, and the finalization and deallocation tutorial, it's still not clear to me what to move to finalize and what to keep in dealloc. The PEP talks about implementation, and less about how to use the finalizers..

What I understood: the actual object freeing must be kept in dealloc (and therefore dealloc is a required method), the finalizer will only be called once, and there is a suggestion to move "Any call to a non-trivial object or API" to finalize. But what is non-trivial here?

  • is calling C functions (that don't call back into Python) trivial?
  • is calling any CPython function non-trivial?
  • and is it safe to change the object state in the finalizer to an invalid state (violating whatever properties the object is expected to maintain during normal life), i.e. is the finalizer guaranteed to be followed by a dealloc?

On a more practical basis, summing all the above: let's consider a simple type that has an attribute that (always) points to a Python object. When tearing down, should the DECREF for that attribute be done in dealloc, or in finalize?

like image 515
iustin Avatar asked Dec 06 '25 18:12

iustin


1 Answers

An old question, but one I was wondering about recently.

Intro

Here's what the docs say (somewhat unclearly), emphasis mine:

There are limitations to what you can safely do in a deallocator function. First, if your type supports garbage collection (using tp_traverse and/or tp_clear), some of the object’s members can have been cleared or finalized by the time tp_dealloc is called. Second, in tp_dealloc, your object is in an unstable state: its reference count is equal to zero.

Any call to a non-trivial object or API (as in the example above) might end up calling tp_dealloc again, causing a double free and a crash. Starting with Python 3.4, it is recommended not to put any complex finalization code in tp_dealloc, and instead use the new tp_finalize type method.

What they are implying here isn't "computational complexity", but "object-graph complexity".

To understand what that means and why it's important, we need to look more deeply at the changes introduced in Python 3.4 by PEP 442 – Safe object finalization.

Definitions

A key part of PEP 442, is the difference between cyclic isolates and cyclic trash.

Here are their definitions, I've emphasized the key difference for this question:

Cyclic isolate (CI) A standalone subgraph of objects in which no object is referenced from the outside, containing one or several reference cycles, and whose objects are still in a usable, non-broken state: they can access each other from their respective finalizers.

Cyclic trash (CT) A former cyclic isolate whose objects have started being cleared by the GC. Objects in cyclic trash are potential zombies; if they are accessed by Python code, the symptoms can vary from weird AttributeErrors to crashes.

In other words, in a cyclic isolate, you can assume the references to its child objects are still valid (in the garbage collector sense). You cannot make this assumption with cyclic trash.

This difference is the key to knowing what you can do in tp_finalize vs tp_dealloc.

What Changed

Here is how clean up used to work:

  1. Weakrefs to CI objects are cleared, and their callbacks called. At this point, the objects are still safe to use.
  2. The CI becomes a CT as the GC systematically breaks all known references inside it (using the tp_clear function).

PEP 442 added two intermediary steps:

  1. Weakrefs to CI objects are cleared, and their callbacks called. At this point, the objects are still safe to use.
  2. NEW The finalizers of all CI objects are called.
  3. NEW The CI is traversed again to determine if it is still isolated. If it is determined that at least one object in CI is now reachable from outside the CI, this collection is aborted and the whole CI is resurrected. Otherwise, proceed.
  4. The CI becomes a CT as the GC systematically breaks all known references inside it (using the tp_clear function).

PEP 442 discusses some important things about the reason for the new step 3 that are worth noting, but right now we're focused on the implication of the new step 2:

tp_finalize can safely traverse and modify the object graph in ways that tp_dealloc cannot.

New Cleanup (AKA, What to Put Where)

Let's say you have an object, A, which has references to objects B and C. Nothing else in memory has references to any of these objects.

A, B, and C are considered cyclic isolates.

This means, within A's tp_finalize method, you can assume that B and C are still usable. You can do any cleanup steps that involve B and C without worrying about the garbage collector deleting them out from under you.

In contrast, within the tp_dealloc method, you have no such guarantee. So if you try doing something with B from A's tp_dealloc method, you may end up working on a zombie object, or manipulating reference counts in a way that triggers a double free.

Summary

So in summary, anything that doesn't use or impact the object graph (including reference counts to objects), can still be cleaned up in tp_dealloc. But anything that uses or impacts the object graph (or has the potential of changing their reference counts) should be moved to tp_finalize.

like image 193
lfalin Avatar answered Dec 11 '25 19:12

lfalin



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!