Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-75459: Doc: C API: Improve object life cycle documentation #125962

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@
*.ico binary
*.jpg binary
*.pck binary
*.pdf binary
*.png binary
*.psd binary
*.tar binary
@@ -67,6 +68,7 @@ PCbuild/readme.txt dos
**/clinic/*.cpp.h generated
**/clinic/*.h.h generated
*_db.h generated
Doc/c-api/lifecycle.dot.svg generated
Doc/data/stable_abi.dat generated
Doc/library/token-list.inc generated
Include/internal/pycore_ast.h generated
127 changes: 102 additions & 25 deletions Doc/c-api/allocation.rst
Original file line number Diff line number Diff line change
@@ -16,46 +16,123 @@ Allocating Objects on the Heap
Initialize a newly allocated object *op* with its type and initial
reference. Returns the initialized object. Other fields of the object are
not affected.
not initialized. Despite its name, this function is unrelated to the
object's :meth:`~object.__init__` method (:c:member:`~PyTypeObject.tp_init`
slot). Specifically, this function does **not** call the object's
:meth:`!__init__` method.
Comment on lines +19 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably worth mentioning that you should just use tp_alloc and not touch this (say it's a "low-level routine").

.. warning::
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer downgrading these to note. These functions are not dangerous.
IMO, from the name it would be more reasonable to think that this calls tp_new or tp_init (thus perhaps setting up some important invariants) than to assume it zeroes memory.

Suggested change
.. warning::
.. note::

(And similar for all the warnings in this PR.)

This function only initializes some of the object's memory. It does not
zero the rest.
.. c:function:: PyVarObject* PyObject_InitVar(PyVarObject *op, PyTypeObject *type, Py_ssize_t size)
This does everything :c:func:`PyObject_Init` does, and also initializes the
length information for a variable-size object.
.. warning::
This function only initializes some of the object's memory. It does not
zero the rest.
.. c:macro:: PyObject_New(TYPE, typeobj)
Allocate a new Python object using the C structure type *TYPE*
and the Python type object *typeobj* (``PyTypeObject*``).
Fields not defined by the Python object header are not initialized.
The caller will own the only reference to the object
(i.e. its reference count will be one).
The size of the memory allocation is determined from the
:c:member:`~PyTypeObject.tp_basicsize` field of the type object.
Allocates a new Python object using the C structure type *TYPE* and the
Python type object *typeobj* (``PyTypeObject*``) by calling
:c:func:`PyObject_Malloc` to allocate memory and initializing it like
:c:func:`PyObject_Init`. The caller will own the only reference to the
object (i.e. its reference count will be one).
Do not call this directly to allocate memory for an object; call the type's
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, "do not" is a strong phrase. Nothing bad will happen if you do it, it's just not recommended.

:c:member:`~PyTypeObject.tp_alloc` slot instead.
When populating a type's :c:member:`~PyTypeObject.tp_alloc` slot,
:c:func:`PyType_GenericAlloc` is preferred over a custom function that
simply calls this macro.
This macro does not call :c:member:`~PyTypeObject.tp_alloc`,
:c:member:`~PyTypeObject.tp_new` (:meth:`~object.__new__`), or
:c:member:`~PyTypeObject.tp_init` (:meth:`~object.__init__`).
This cannot be used for objects with :c:macro:`Py_TPFLAGS_HAVE_GC` set in
:c:member:`~PyTypeObject.tp_flags`; use :c:macro:`PyObject_GC_New` instead.
Memory allocated by this function must be freed with :c:func:`PyObject_Free`
(usually called via the object's :c:member:`~PyTypeObject.tp_free` slot).
.. warning::
The returned memory is not guaranteed to have been completely zeroed
before it was initialized.
.. warning::
This macro does not construct a fully initialized object of the given
type; it merely allocates memory and prepares it for further
initialization by :c:member:`~PyTypeObject.tp_init`. To construct a
fully initialized object, call *typeobj* instead. For example::
PyObject *foo = PyObject_CallNoArgs((PyObject *)&PyFoo_Type);
Note that this function is unsuitable if *typeobj* has
:c:macro:`Py_TPFLAGS_HAVE_GC` set. For such objects,
use :c:func:`PyObject_GC_New` instead.
.. seealso::

* :c:func:`PyObject_Free`
* :c:macro:`PyObject_GC_New`
* :c:func:`PyType_GenericAlloc`
* :c:member:`~PyTypeObject.tp_alloc`


.. c:macro:: PyObject_NewVar(TYPE, typeobj, size)
Allocate a new Python object using the C structure type *TYPE* and the
Python type object *typeobj* (``PyTypeObject*``).
Fields not defined by the Python object header
are not initialized. The allocated memory allows for the *TYPE* structure
plus *size* (``Py_ssize_t``) fields of the size
given by the :c:member:`~PyTypeObject.tp_itemsize` field of
*typeobj*. This is useful for implementing objects like tuples, which are
able to determine their size at construction time. Embedding the array of
fields into the same allocation decreases the number of allocations,
improving the memory management efficiency.
Note that this function is unsuitable if *typeobj* has
:c:macro:`Py_TPFLAGS_HAVE_GC` set. For such objects,
use :c:func:`PyObject_GC_NewVar` instead.
Like :c:macro:`PyObject_New` except:

* It allocates enough memory for the *TYPE* structure plus *size*
(``Py_ssize_t``) fields of the size given by the
:c:member:`~PyTypeObject.tp_itemsize` field of *typeobj*.
* The memory is initialized like :c:func:`PyObject_InitVar`.

This is useful for implementing objects like tuples, which are able to
determine their size at construction time. Embedding the array of fields
into the same allocation decreases the number of allocations, improving the
memory management efficiency.

Do not call this directly to allocate memory for an object; call the type's
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. (This occurs several times, so just deal with all of them.)

:c:member:`~PyTypeObject.tp_alloc` slot instead.

When populating a type's :c:member:`~PyTypeObject.tp_alloc` slot,
:c:func:`PyType_GenericAlloc` is preferred over a custom function that
simply calls this macro.

This cannot be used for objects with :c:macro:`Py_TPFLAGS_HAVE_GC` set in
:c:member:`~PyTypeObject.tp_flags`; use :c:macro:`PyObject_GC_NewVar`
instead.

Memory allocated by this function must be freed with :c:func:`PyObject_Free`
(usually called via the object's :c:member:`~PyTypeObject.tp_free` slot).

.. warning::

The returned memory is not guaranteed to have been completely zeroed
before it was initialized.

.. warning::

This macro does not construct a fully initialized object of the given
type; it merely allocates memory and prepares it for further
initialization by :c:member:`~PyTypeObject.tp_init`. To construct a
fully initialized object, call *typeobj* instead. For example::

PyObject *foo = PyObject_CallNoArgs((PyObject *)&PyFoo_Type);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's worth using an example of a real type here, primarily because not all types are available via PySomething_Type.

Suggested change
PyObject *foo = PyObject_CallNoArgs((PyObject *)&PyFoo_Type);
PyObject *list_instance = PyObject_CallNoArgs((PyObject *)&PyList_Type);


.. seealso::

* :c:func:`PyObject_Free`
* :c:macro:`PyObject_GC_NewVar`
* :c:func:`PyType_GenericAlloc`
* :c:member:`~PyTypeObject.tp_alloc`


.. c:function:: void PyObject_Del(void *op)
57 changes: 57 additions & 0 deletions Doc/c-api/gcsupport.rst
Original file line number Diff line number Diff line change
@@ -57,11 +57,49 @@ rules:
Analogous to :c:macro:`PyObject_New` but for container objects with the
:c:macro:`Py_TPFLAGS_HAVE_GC` flag set.

Do not call this directly to allocate memory for an object; call the type's
:c:member:`~PyTypeObject.tp_alloc` slot instead.

When populating a type's :c:member:`~PyTypeObject.tp_alloc` slot,
:c:func:`PyType_GenericAlloc` is preferred over a custom function that
simply calls this macro.

Memory allocated by this function must be freed with
:c:func:`PyObject_GC_Del` (usually called via the object's
:c:member:`~PyTypeObject.tp_free` slot).

.. seealso::

* :c:func:`PyObject_GC_Del`
* :c:macro:`PyObject_New`
* :c:func:`PyType_GenericAlloc`
* :c:member:`~PyTypeObject.tp_alloc`


.. c:macro:: PyObject_GC_NewVar(TYPE, typeobj, size)
Analogous to :c:macro:`PyObject_NewVar` but for container objects with the
:c:macro:`Py_TPFLAGS_HAVE_GC` flag set.

Do not call this directly to allocate memory for an object; call the type's
:c:member:`~PyTypeObject.tp_alloc` slot instead.

When populating a type's :c:member:`~PyTypeObject.tp_alloc` slot,
:c:func:`PyType_GenericAlloc` is preferred over a custom function that
simply calls this macro.

Memory allocated by this function must be freed with
:c:func:`PyObject_GC_Del` (usually called via the object's
:c:member:`~PyTypeObject.tp_free` slot).

.. seealso::

* :c:func:`PyObject_GC_Del`
* :c:macro:`PyObject_NewVar`
* :c:func:`PyType_GenericAlloc`
* :c:member:`~PyTypeObject.tp_alloc`


.. c:function:: PyObject* PyUnstable_Object_GC_NewWithExtraData(PyTypeObject *type, size_t extra_size)
Analogous to :c:macro:`PyObject_GC_New` but allocates *extra_size*
@@ -73,6 +111,10 @@ rules:
The extra data will be deallocated with the object, but otherwise it is
not managed by Python.
Memory allocated by this function must be freed with
:c:func:`PyObject_GC_Del` (usually called via the object's
:c:member:`~PyTypeObject.tp_free` slot).
.. warning::
The function is marked as unstable because the final mechanism
for reserving extra data after an instance is not yet decided.
@@ -136,6 +178,21 @@ rules:
Releases memory allocated to an object using :c:macro:`PyObject_GC_New` or
:c:macro:`PyObject_GC_NewVar`.
Do not call this directly to free an object's memory; call the type's
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm not a fan of "do not" here again. This "do not" is not nearly the severity of the case below, in which you would get a segfault.

:c:member:`~PyTypeObject.tp_free` slot instead.
Do not use this for memory allocated by :c:macro:`PyObject_New`,
:c:macro:`PyObject_NewVar`, or related allocation functions; use
:c:func:`PyObject_Free` instead.
.. seealso::
* :c:func:`PyObject_Free` is the non-GC equivalent of this function.
* :c:macro:`PyObject_GC_New`
* :c:macro:`PyObject_GC_NewVar`
* :c:func:`PyType_GenericAlloc`
* :c:member:`~PyTypeObject.tp_free`
.. c:function:: void PyObject_GC_UnTrack(void *op)
158 changes: 158 additions & 0 deletions Doc/c-api/lifecycle.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
digraph "Life Events" {
graph [
fontnames="svg"
fontsize=12.0
id="life_events_graph"
layout="dot"
margin="0,0"
ranksep=0.25
stylesheet="lifecycle.dot.css"
]
node [
fontname="Courier"
fontsize=12.0
]
edge [
fontname="Times-Italic"
fontsize=12.0
]

"start" [fontname="Times-Italic" shape=plain label=< start > style=invis]
{
rank="same"
"tp_new" [href="typeobj.html#c.PyTypeObject.tp_new" target="_top"]
"tp_alloc" [href="typeobj.html#c.PyTypeObject.tp_alloc" target="_top"]
}
"tp_init" [href="typeobj.html#c.PyTypeObject.tp_init" target="_top"]
"reachable" [fontname="Times-Italic" shape=box]
"tp_traverse" [
href="typeobj.html#c.PyTypeObject.tp_traverse"
ordering="in"
target="_top"
]
"finalized?" [
fontname="Times-Italic"
label=<marked as<br/>finalized?>
ordering="in"
shape=diamond
tooltip="marked as finalized?"
]
"tp_finalize" [
href="typeobj.html#c.PyTypeObject.tp_finalize"
ordering="in"
target="_top"
]
"tp_clear" [href="typeobj.html#c.PyTypeObject.tp_clear" target="_top"]
"uncollectable" [
fontname="Times-Italic"
label=<uncollectable<br/>(leaked)>
shape=box
tooltip="uncollectable (leaked)"
]
"tp_dealloc" [
href="typeobj.html#c.PyTypeObject.tp_dealloc"
ordering="in"
target="_top"
]
"tp_free" [href="typeobj.html#c.PyTypeObject.tp_free" target="_top"]

"start" -> "tp_new" [
label=< type call >
labeltooltip="start to tp_new: type call"
tooltip="start to tp_new: type call"
Comment on lines +61 to +62
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove labeltooltip and tooltip in the whole diagram. They don't add any information, but they do make non-link text underlined on mouse hover.

Suggested change
labeltooltip="start to tp_new: type call"
tooltip="start to tp_new: type call"

]
"tp_new" -> "tp_alloc" [
label=< direct call > arrowhead=empty
labeltooltip="tp_new to tp_alloc: direct call"
tooltip="tp_new to tp_alloc: direct call"
]
"tp_new" -> "tp_init" [tooltip="tp_new to tp_init"]
"tp_init" -> "reachable" [tooltip="tp_init to reachable"]
"reachable" -> "tp_traverse" [
dir="back"
label=< not in a <br/> cyclic <br/> isolate >
labeltooltip="tp_traverse to reachable: not in a cyclic isolate"
tooltip="tp_traverse to reachable: not in a cyclic isolate"
]
"reachable" -> "tp_traverse" [
label=< periodic <br/> cyclic isolate <br/> detection >
labeltooltip="reachable to tp_traverse: periodic cyclic isolate detection"
tooltip="reachable to tp_traverse: periodic cyclic isolate detection"
]
"reachable" -> "tp_init" [tooltip="reachable to tp_init"]
"reachable" -> "tp_finalize" [
dir="back"
label=< resurrected <br/> (maybe remove <br/> finalized mark) >
labeltooltip="tp_finalize to reachable: resurrected (maybe remove finalized mark)"
tooltip="tp_finalize to reachable: resurrected (maybe remove finalized mark)"
]
"tp_traverse" -> "finalized?" [
label=< cyclic <br/> isolate >
labeltooltip="tp_traverse to finalized?: cyclic isolate"
tooltip="tp_traverse to finalized?: cyclic isolate"
]
"reachable" -> "finalized?" [
label=< no refs >
labeltooltip="reachable to finalized?: no refs"
tooltip="reachable to finalized?: no refs"
]
"finalized?" -> "tp_finalize" [
label=< no (mark <br/> as finalized) >
labeltooltip="finalized? to tp_finalize: no (mark as finalized)"
tooltip="finalized? to tp_finalize: no (mark as finalized)"
]
"finalized?" -> "tp_clear" [
label=< yes >
labeltooltip="finalized? to tp_clear: yes"
tooltip="finalized? to tp_clear: yes"
]
"tp_finalize" -> "tp_clear" [
label=< no refs or <br/> cyclic isolate >
labeltooltip="tp_finalize to tp_clear: no refs or cyclic isolate"
tooltip="tp_finalize to tp_clear: no refs or cyclic isolate"
]
"tp_finalize" -> "tp_dealloc" [
arrowtail=empty
dir="back"
href="lifecycle.html#c.PyObject_CallFinalizerFromDealloc"
style=dashed
label=< recommended<br/> call (see<br/> explanation)>
labeltooltip="tp_dealloc to tp_finalize: recommended call (see explanation)"
target="_top"
tooltip="tp_dealloc to tp_finalize: recommended call (see explanation)"
]
"tp_finalize" -> "tp_dealloc" [
label=< no refs >
labeltooltip="tp_finalize to tp_dealloc: no refs"
tooltip="tp_finalize to tp_dealloc: no refs"
]
"tp_clear" -> "tp_dealloc" [
label=< no refs >
labeltooltip="tp_clear to tp_dealloc: no refs"
tooltip="tp_clear to tp_dealloc: no refs"
]
"tp_clear" -> "uncollectable" [
label=< cyclic <br/> isolate >
labeltooltip="tp_clear to uncollectable: cyclic isolate"
tooltip="tp_clear to uncollectable: cyclic isolate"
]
"uncollectable" -> "tp_dealloc" [
style=invis
tooltip="uncollectable to tp_dealloc"
]
"reachable" -> "uncollectable" [
label=< cyclic <br/> isolate <br/> (no GC <br/> support) >
labeltooltip="reachable to uncollectable: cyclic isolate (no GC support)"
tooltip="reachable to uncollectable: cyclic isolate (no GC support)"
]
"reachable" -> "tp_dealloc" [
label=< no refs>
labeltooltip="reachable to tp_dealloc: no refs"
]
"tp_dealloc" -> "tp_free" [
arrowhead=empty
label=< direct call >
labeltooltip="tp_dealloc to tp_free: direct call"
tooltip="tp_dealloc to tp_free: direct call"
]
}
41 changes: 41 additions & 0 deletions Doc/c-api/lifecycle.dot.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#life_events_graph {
/*
* Unfortunately these colors don't seem to be exposed in any of the theme's
* variables, so they are manually copied here.
*/
--svg-light-fgcolor: black;
--svg-light-bgcolor: white;
--svg-dark-fgcolor: rgba(255, 255, 255, 0.87);
--svg-dark-bgcolor: #222;
--svg-fgcolor: var(--svg-light-fgcolor);
--svg-bgcolor: var(--svg-light-bgcolor);
}
@media (prefers-color-scheme: dark) {
#life_events_graph {
--svg-fgcolor: var(--svg-dark-fgcolor);
--svg-bgcolor: var(--svg-dark-bgcolor);
}
}
@media (prefers-color-scheme: light) {
#life_events_graph {
--svg-fgcolor: var(--svg-light-fgcolor);
--svg-bgcolor: var(--svg-light-bgcolor);
}
}
:root:has(#pydoctheme_dark_css[media="not all"]) #life_events_graph {
--svg-fgcolor: var(--svg-light-fgcolor);
--svg-bgcolor: var(--svg-light-bgcolor);
}
:root:has(#pydoctheme_dark_css[media="all"]) #life_events_graph {
--svg-fgcolor: var(--svg-dark-fgcolor);
--svg-bgcolor: var(--svg-dark-bgcolor);
}
#life_events_graph [stroke="black"] {
stroke: var(--svg-fgcolor);
}
#life_events_graph :is(text, [fill="black"]) {
fill: var(--svg-fgcolor);
}
#life_events_graph :is([fill="white"]) {
fill: var(--svg-bgcolor);
}
Comment on lines +1 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified using currentcolor & transparent:

Suggested change
#life_events_graph {
/*
* Unfortunately these colors don't seem to be exposed in any of the theme's
* variables, so they are manually copied here.
*/
--svg-light-fgcolor: black;
--svg-light-bgcolor: white;
--svg-dark-fgcolor: rgba(255, 255, 255, 0.87);
--svg-dark-bgcolor: #222;
--svg-fgcolor: var(--svg-light-fgcolor);
--svg-bgcolor: var(--svg-light-bgcolor);
}
@media (prefers-color-scheme: dark) {
#life_events_graph {
--svg-fgcolor: var(--svg-dark-fgcolor);
--svg-bgcolor: var(--svg-dark-bgcolor);
}
}
@media (prefers-color-scheme: light) {
#life_events_graph {
--svg-fgcolor: var(--svg-light-fgcolor);
--svg-bgcolor: var(--svg-light-bgcolor);
}
}
:root:has(#pydoctheme_dark_css[media="not all"]) #life_events_graph {
--svg-fgcolor: var(--svg-light-fgcolor);
--svg-bgcolor: var(--svg-light-bgcolor);
}
:root:has(#pydoctheme_dark_css[media="all"]) #life_events_graph {
--svg-fgcolor: var(--svg-dark-fgcolor);
--svg-bgcolor: var(--svg-dark-bgcolor);
}
#life_events_graph [stroke="black"] {
stroke: var(--svg-fgcolor);
}
#life_events_graph :is(text, [fill="black"]) {
fill: var(--svg-fgcolor);
}
#life_events_graph :is([fill="white"]) {
fill: var(--svg-bgcolor);
}
#life_events_graph {
--svg-fgcolor: currentcolor;
--svg-bgcolor: transparent;
}
#life_events_graph a {
color: inherit;
}
#life_events_graph [stroke="black"] {
stroke: var(--svg-fgcolor);
}
#life_events_graph text,
#life_events_graph [fill="black"] {
fill: var(--svg-fgcolor);
}
#life_events_graph [fill="white"] {
fill: var(--svg-bgcolor);
}
#life_events_graph [fill="none"] {
/* On links, setting fill will make the entire shape clickable */
fill: var(--svg-bgcolor);
}

Binary file added Doc/c-api/lifecycle.dot.pdf
Binary file not shown.
374 changes: 374 additions & 0 deletions Doc/c-api/lifecycle.dot.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
268 changes: 268 additions & 0 deletions Doc/c-api/lifecycle.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
.. highlight:: c

.. _life-cycle:

Object Life Cycle
=================

This section explains how a type's slots relate to each other throughout the
life of an object. It is not intended to be a complete canonical reference for
the slots; instead, refer to the slot-specific documentation in
:ref:`type-structs` for details about a particular slot.


Life Events
-----------

The figure below illustrates the order of events that can occur throughout an
object's life. An arrow from *A* to *B* indicates that event *B* can occur
after event *A* has occurred, with the arrow's label indicating the condition
that must be true for *B* to occur after *A*.

.. only:: html and not epub

.. raw:: html

<style type="text/css">
.. raw:: html
:file: lifecycle.dot.css
.. raw:: html
</style>
.. raw:: html
:file: lifecycle.dot.svg
.. raw:: html
<script>
(() => {
const g = document.getElementById('life_events_graph');
const title = g.querySelector(':scope > title');
title.id = 'life-events-graph-title';
const svg = g.closest('svg');
svg.role = 'img';
svg.setAttribute('aria-describedby',
'life-events-graph-description');
svg.setAttribute('aria-labelledby', 'life-events-graph-title');
})();
</script>
.. only:: epub or not (html or latex)
.. image:: lifecycle.dot.svg
:align: center
:class: invert-in-dark-mode
:alt: Diagram showing events in an object's life. Explained in detail
below.
.. only:: latex
.. image:: lifecycle.dot.pdf
:align: center
:class: invert-in-dark-mode
:alt: Diagram showing events in an object's life. Explained in detail
below.
.. container::
:name: life-events-graph-description
Explanation:
* When a new object is constructed by calling its type:
#. :c:member:`~PyTypeObject.tp_new` is called to create a new object.
#. :c:member:`~PyTypeObject.tp_alloc` is directly called by
:c:member:`~PyTypeObject.tp_new` to allocate the memory for the new
object.
#. :c:member:`~PyTypeObject.tp_init` initializes the newly created object.
:c:member:`!tp_init` can be called again to re-initialize an object, if
desired.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tp_new can be skipped:

Suggested change
desired.
desired. The :c:member:`!tp_init` call can also be skipped entirely,
for example by Python code calling :py:meth:`~object.__new__`.

This also needs an arrow in the diagram.

* After :c:member:`!tp_init` completes, the object is ready to use.
* After the last reference to an object is removed:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* After the last reference to an object is removed:
* Some time after the last reference to an object is removed:

#. If an object is not marked as *finalized*, it might be finalized by
marking it as *finalized* and calling its
:c:member:`~PyTypeObject.tp_finalize` function. Python currently does
not finalize an object when the last reference to it is deleted, but
this may change in the future.
Comment on lines +89 to +91
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should add something like "(unless PyObject_CallFinalizerFromDealloc is used in the destructor)"

#. If the object is marked as finalized,
:c:member:`~PyTypeObject.tp_clear` might be called to clear references
held by the object. Python currently does not clear an object in
response to the deletion of the last reference, but this may change in
the future.
#. :c:member:`~PyTypeObject.tp_dealloc` is called to destroy the object.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we want to say that it's good practice to just have tp_dealloc call into the tp_clear slot?

#. When :c:member:`~PyTypeObject.tp_dealloc` finishes object destruction,
it directly calls :c:member:`~PyTypeObject.tp_free` (usually set to
:c:func:`PyObject_Free` or :c:func:`PyObject_GC_Del` automatically as
appropriate for the type) to deallocate the memory.
* The :c:member:`~PyTypeObject.tp_finalize` function is permitted to add a
reference to the object if desired. If it does, the object is
*resurrected*, preventing its pending destruction. (Only
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is "resurrected" italicized?

:c:member:`!tp_finalize` is allowed to resurrect an object;
:c:member:`~PyTypeObject.tp_clear` and
:c:member:`~PyTypeObject.tp_dealloc` cannot.) Resurrecting an object may
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, tp_dealloc can through PyObject_CallFinalizerFromDealloc.

Comment on lines +105 to +108
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would make the text contradictory: if tp_finalize may resurrect an object but tp_dealloc may not, then tp_dealloc may call PyObject_CallFinalizerFromDealloc (which calls tp_finalize).

AFAIK, any of these may resurrect the object.

or may not cause the object's *finalized* mark to be removed. Currently,
Python does not remove the *finalized* mark from a resurrected object if
it supports garbage collection (i.e., the :c:macro:`Py_TPFLAGS_HAVE_GC`
flag is set) but does remove the mark if the object does not support
garbage collection; either or both of these behaviors may change in the
future.
* :c:member:`~PyTypeObject.tp_dealloc` can optionally call
:c:member:`~PyTypeObject.tp_finalize` via
:c:func:`PyObject_CallFinalizerFromDealloc` if it wishes to reuse that
code to help with object destruction. This is recommended because it
guarantees that :c:member:`!tp_finalize` is always called before
destruction. See the :c:member:`~PyTypeObject.tp_dealloc` documentation
for example code.
Comment on lines +115 to +121
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tp_clear documentation also suggests that tp_dealloc can call tp_clear, to avoid code duplication.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK:

For heap types (ones defined in Python code, and ones defined via PyType_From* functions), tp_dealloc defaults to a function called subtype_dealloc, which does the whole dance: calls PyObject_CallFinalizerFromDealloc and PyObject_GC_UnTrack, __dict__ and delegates to a supertype's dealloc.

For these types, we should, strongly recommend to not implement your own tp_dealloc.

tp_dealloc is what's set when a user defines __del__ in Python code. That's why it's important to call it once, and always: users expect their custom code to run!

However, it's not that important to call in on types you control, where you know users can't provide __del__ -- that is, on immutable types (Py_TPFLAGS_IMMUTABLETYPE).

Note that all static (i.e. non-heap) types are immutable.

So, the recommendation to call PyObject_CallFinalizerFromDealloc is only applicable when you're defining a heap type with custom tp_dealloc -- which should not be recommended.

If you're looking to write tp_finalize as a C function, a better recommendation would, IMO, be to instead put the clearing code in tp_clear, and call that from tp_dealloc (making sure that your tp_clear is idempotent). I see this PR already makes that recommendation.

* If the object is a member of a :term:`cyclic isolate` and either
:c:member:`~PyTypeObject.tp_clear` fails to break the reference cycle or
the cyclic isolate is not detected (perhaps :func:`gc.disable` was called,
or the :c:macro:`Py_TPFLAGS_HAVE_GC` flag was erroneously omitted in one
of the involved types), the objects remain indefinitely uncollectable
(they "leak"). See :data:`gc.garbage`.
If the object is marked as supporting garbage collection (the
:c:macro:`Py_TPFLAGS_HAVE_GC` flag is set in
:c:member:`~PyTypeObject.tp_flags`), the following events are also possible:
* The garbage collector occasionally calls
:c:member:`~PyTypeObject.tp_traverse` to identify :term:`cyclic isolates
<cyclic isolate>`.
* When the garbage collector discovers a :term:`cyclic isolate`, it
finalizes one of the objects in the group by marking it as *finalized* and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like I've already asked this, but why is this italicized?

calling its :c:member:`~PyTypeObject.tp_finalize` function, if it has one.
This repeats until the cyclic isolate doesn't exist or all of the objects
have been finalized.
* :c:member:`~PyTypeObject.tp_finalize` is permitted to resurrect the object
by adding a reference from outside the :term:`cyclic isolate`. The new
reference causes the group of objects to no longer form a cyclic isolate
(the reference cycle may still exist, but if it does the objects are no
longer isolated).
* When the garbage collector discovers a :term:`cyclic isolate` and all of
the objects in the group have already been marked as *finalized*, the
garbage collector clears one or more of the uncleared objects in the group
(possibly concurrently, but with the :term:`GIL` held) by calling each's
:c:member:`~PyTypeObject.tp_clear` function. This repeats as long as the
cyclic isolate still exists and not all of the objects have been cleared.
Cyclic Isolate Destruction
--------------------------
Listed below are the stages of life of a hypothetical :term:`cyclic isolate`
that continues to exist after each member object is finalized or cleared. It
is a bug if a cyclic isolate progresses through all of these stages; it should
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"memory leak" instead of "bug" is more informative:

Suggested change
is a bug if a cyclic isolate progresses through all of these stages; it should
is a memory leak if a cyclic isolate progresses through all of these stages; it should

vanish once all objects are cleared, if not sooner. A cyclic isolate can
vanish either because the reference cycle is broken or because the objects are
no longer isolated due to finalizer resurrection (see
:c:member:`~PyTypeObject.tp_finalize`).
0. **Reachable** (not yet a cyclic isolate): All objects are in their normal,
reachable state. A reference cycle could exist, but an external reference
means the objects are not yet isolated.
#. **Unreachable but consistent:** The final reference from outside the cyclic
group of objects has been removed, causing the objects to become isolated
(thus a cyclic isolate is born). None of the group's objects have been
finalized or cleared yet. The cyclic isolate remains at this stage until
some future run of the garbage collector (not necessarily the next run
because the next run might not scan every object).
#. **Mix of finalized and not finalized:** Objects in a cyclic isolate are
finalized one at a time, which means that there is a period of time when the
cyclic isolate is composed of a mix of finalized and non-finalized objects.
Finalization order is unspecified, so it can appear random. A finalized
object must behave in a sane manner when non-finalized objects interact with
it, and a non-finalized object must be able to tolerate the finalization of
an arbitrary subset of its referents.
#. **All finalized:** All objects in a cyclic isolate are finalized before any
of them are cleared.
#. **Mix of finalized and cleared:** The objects can be cleared serially or
concurrently (but with the :term:`GIL` held); either way, some will finish
before others. A finalized object must be able to tolerate the clearing of
a subset of its referents. :pep:`442` calls this stage "cyclic trash".
#. **Leaked:** If a cyclic isolate still exists after all objects in the group
have been finalized and cleared, then the objects remain indefinitely
uncollectable (see :data:`gc.garbage`). It is a bug if a cyclic isolate
reaches this stage---it means the :c:member:`~PyTypeObject.tp_clear` methods
of the participating objects have failed to break the reference cycle as
required.
If :c:member:`~PyTypeObject.tp_clear` did not exist, then Python would have no
way to safely break a reference cycle. Simply destroying an object in a cyclic
isolate would result in a dangling pointer, triggering undefined behavior when
an object referencing the destroyed object is itself destroyed. The clearing
step makes object destruction a two-phase process: first
:c:member:`~PyTypeObject.tp_clear` is called to partially destroy the objects
enough to detangle them from each other, then
:c:member:`~PyTypeObject.tp_dealloc` is called to complete the destruction.
Unlike clearing, finalization is not a phase of destruction. A finalized
object must still behave properly by continuing to fulfill its design
contracts. An object's finalizer is allowed to execute arbitrary Python code,
and is even allowed to prevent the impending destruction by adding a reference.
The finalizer is only contemporaneously related to destruction---it runs just
before destruction, which starts with :c:member:`~PyTypeObject.tp_clear` (if
called) and concludes with :c:member:`~PyTypeObject.tp_dealloc`.
The finalization step is not necessary to safely reclaim the objects in a
cyclic isolate, but its existence makes it easier to design types that behave
in a sane manner when objects are cleared. Clearing an object might
necessarily leave it in a broken, partially destroyed state---it might be
unsafe to call any of the cleared object's methods or access any of its
attributes. With finalization, only finalized objects can possibly interact
with cleared objects; non-finalized objects are guaranteed to interact with
only non-cleared (but potentially finalized) objects.
To summarize the possible interactions:
* A non-finalized object might have references to or from non-finalized and
finalized objects, but not to or from cleared objects.
* A finalized object might have references to or from non-finalized, finalized,
and cleared objects.
* A cleared object might have references to or from finalized and cleared
objects, but not to or from non-finalized objects.
Without any reference cycles, an object can be simply destroyed once its last
reference is deleted; the finalization and clearing steps are not necessary to
safely reclaim unused objects. However, it can be useful to automatically call
:c:member:`~PyTypeObject.tp_finalize` and :c:member:`~PyTypeObject.tp_clear`
before destruction anyway because type design is simplified when all objects
always experience the same series of events regardless of whether they
participated in a cyclic isolate. Python currently only calls
:c:member:`~PyTypeObject.tp_finalize` and :c:member:`~PyTypeObject.tp_clear` as
needed to destroy a cyclic isolate; this may change in a future version.
Functions
---------
To allocate and free memory, see :ref:`allocating-objects`.
.. c:function:: void PyObject_CallFinalizer(PyObject *op)
Finalizes the object as described in :c:member:`~PyTypeObject.tp_finalize`.
Call this function (or :c:func:`PyObject_CallFinalizerFromDealloc`) instead
of calling :c:member:`~PyTypeObject.tp_finalize` directly because this
function may deduplicate multiple calls to :c:member:`!tp_finalize`.
Currently, calls are only deduplicated if the type supports garbage
collection (i.e., the :c:macro:`Py_TPFLAGS_HAVE_GC` flag is set); this may
change in the future.
.. c:function:: int PyObject_CallFinalizerFromDealloc(PyObject *op)
Same as :c:func:`PyObject_CallFinalizer` but meant to be called at the
beginning of the object's destructor (:c:member:`~PyTypeObject.tp_dealloc`).
There must not be any references to the object. If the object's finalizer
resurrects the object, this function returns -1; no further destruction
should happen. Otherwise, this function returns 0 and destruction can
continue normally.
.. seealso::
:c:member:`~PyTypeObject.tp_dealloc` for example code.
18 changes: 18 additions & 0 deletions Doc/c-api/memory.rst
Original file line number Diff line number Diff line change
@@ -378,6 +378,24 @@ The :ref:`default object allocator <default-memory-allocators>` uses the
If *p* is ``NULL``, no operation is performed.
Do not call this directly to free an object's memory; call the type's
:c:member:`~PyTypeObject.tp_free` slot instead.
Do not use this for memory allocated by :c:macro:`PyObject_GC_New` or
:c:macro:`PyObject_GC_NewVar`; use :c:func:`PyObject_GC_Del` instead.
.. seealso::
* :c:func:`PyObject_GC_Del` is the equivalent of this function for memory
allocated by types that support garbage collection.
* :c:func:`PyObject_Malloc`
* :c:func:`PyObject_Realloc`
* :c:func:`PyObject_Calloc`
* :c:macro:`PyObject_New`
* :c:macro:`PyObject_NewVar`
* :c:func:`PyType_GenericAlloc`
* :c:member:`~PyTypeObject.tp_free`
.. _default-memory-allocators:
1 change: 1 addition & 0 deletions Doc/c-api/objimpl.rst
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ object types.
.. toctree::

allocation.rst
lifecycle.rst
structures.rst
typeobj.rst
gcsupport.rst
25 changes: 20 additions & 5 deletions Doc/c-api/type.rst
Original file line number Diff line number Diff line change
@@ -148,14 +148,29 @@ Type Objects
.. c:function:: PyObject* PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems)
Generic handler for the :c:member:`~PyTypeObject.tp_alloc` slot of a type object. Use
Python's default memory allocation mechanism to allocate a new instance and
initialize all its contents to ``NULL``.
Generic handler for the :c:member:`~PyTypeObject.tp_alloc` slot of a type
object. Uses Python's default memory allocation mechanism to allocate memory
for a new instance, zeros the memory, then initializes the memory as if by
calling :c:func:`PyObject_Init` or :c:func:`PyObject_InitVar`.
Do not call this directly to allocate memory for an object; call the type's
:c:member:`~PyTypeObject.tp_alloc` slot instead.
For types that support garbage collection (i.e., the
:c:macro:`Py_TPFLAGS_HAVE_GC` flag is set), this function behaves like
:c:macro:`PyObject_GC_New` or :c:macro:`PyObject_GC_NewVar` (except the
memory is guaranteed to be zeroed before initialization), and should be
paired with :c:func:`PyObject_GC_Del` in :c:member:`~PyTypeObject.tp_free`.
Otherwise, it behaves like :c:macro:`PyObject_New` or
:c:macro:`PyObject_NewVar` (except the memory is guaranteed to be zeroed
before initialization) and should be paired with :c:func:`PyObject_Free` in
:c:member:`~PyTypeObject.tp_free`.
.. c:function:: PyObject* PyType_GenericNew(PyTypeObject *type, PyObject *args, PyObject *kwds)
Generic handler for the :c:member:`~PyTypeObject.tp_new` slot of a type object. Create a
new instance using the type's :c:member:`~PyTypeObject.tp_alloc` slot.
Generic handler for the :c:member:`~PyTypeObject.tp_new` slot of a type
object. Creates a new instance using the type's
:c:member:`~PyTypeObject.tp_alloc` slot and returns the resulting object.
.. c:function:: int PyType_Ready(PyTypeObject *type)
454 changes: 346 additions & 108 deletions Doc/c-api/typeobj.rst

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions Doc/glossary.rst
Original file line number Diff line number Diff line change
@@ -333,6 +333,12 @@ Glossary
tasks (see :mod:`asyncio`) associate each task with a context which
becomes the current context whenever the task starts or resumes execution.

cyclic isolate
A subgroup of one or more objects that reference each other in a reference
cycle, but are not referenced by objects outside the group. The goal of
the garbage collector is to identify these groups and break the reference
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
the garbage collector is to identify these groups and break the reference
the :term:`cyclic garbage collector <garbage collection>` is to identify these groups and break the reference

cycles so that the memory can be reclaimed.

decorator
A function returning another function, usually applied as a function
transformation using the ``@wrapper`` syntax. Common examples for