diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d9a9f9fca2d..5834e24f1d8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -673,6 +673,7 @@ peps/pep-0791.rst @vstinner peps/pep-0792.rst @dstufft peps/pep-0793.rst @encukou peps/pep-0794.rst @brettcannon +peps/pep-0797.rst @ZeroIntensity peps/pep-0798.rst @JelleZijlstra # ... peps/pep-0801.rst @warsaw diff --git a/peps/pep-0797.rst b/peps/pep-0797.rst new file mode 100644 index 00000000000..ee70a585d97 --- /dev/null +++ b/peps/pep-0797.rst @@ -0,0 +1,672 @@ +PEP: 797 +Title: Arbitrary Object Immortalization +Author: Peter Bierma +Discussions-To: Pending +Status: Draft +Type: Standards Track +Requires: 683 +Created: 16-Jul-2025 +Python-Version: 3.15 +Post-History: `01-Jul-2025 `__ + +Abstract +======== + +This PEP aims to introduce a public way to immortalize arbitrary objects, while +making sure the object is deallocated later (during interpreter +finalization). This is done by introducing two new APIs: :func:`sys._immortalize` +on the Python side, and :c:func:`PyUnstable_Immortalize` on the C side. These +are generally considered to be low-level APIs that won't be needed by users; the +main beneficiary will be CPython. + +Both of these functions make the given object immortal, and then deallocate it when +the interpreter is shutting down using a simulated garbage collection. + +For example: + +.. code-block:: C + + static PyObject * + my_singleton_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) + { + PyObject *self = type->tp_alloc(type, 0); + if (self == NULL) { + return NULL; + } + + if (PyUnstable_Immortalize(self) < 0) { + return NULL; + } + + return self; + } + +Or, in Python: + +.. code-block:: python + + import sys + + my_singleton = object() + sys._immortalize(my_singleton) + assert sys._is_immortal(my_singleton) + +Running either of these examples on a Python debug build with :option:`-X showrefcount <-X>` +show that the (heap-allocated) immortal object is not leaked: + +.. code-block:: bash + + $ ./python -Xshowrefcount -c "import sys; sys._immortalize(object())" + [0 refs, 0 blocks] + +Motivation +========== + +It's Currently Impossible to Immortalize General Objects +******************************************************** + +:pep:`683` introduced :term:`immortal` objects into CPython, using an "immortal +reference count" where the reference count field is never modified on the +object. Currently, all APIs for enabling immortality on a given object are +private, and even those APIs aren't fully safe for immortalizing *any* object. + +In particular, the private API has ``_Py_SetImmortal``, which simply untracks a +passed object from the garbage collector, and then sets its reference count to +the magic immortal number. But again, this isn't totally safe; for example, due +to the current string interning implementation, immortalizing a string can cause +crashes. To make matters worse, there's no mechanism in place to deallocate an +object once it has become immortal; this is limiting for potential optimizations +using immortality, because immortal objects stored on the heap (and the memory +that they own) are leaked. + +What Problem Does Immortality Solve? +************************************ + +Currently, the main way to share objects between subinterpreters is via +serialization. This works OK, but can be limited for many objects. +Immortality is a necessary precursor to directly sharing objects across multiple +interpreters, as it provides a way to make reference counting thread-safe +without atomic operations on the reference count field (which has proven to +drastically hurt performance). + +Subinterpreters aside, immortality is just generally a powerful tool, +especially for CPython maintainers. The authors of this PEP do not expect that +immortality will become a common tool for Python code, but the complexity of +the approach on its own warrants a PEP. + +Speculatively, arbitrary object immortalization could prove to be useful in +other areas of CPython, or in third-party libraries: + +- Some (especially user-defined) objects on the :term:`free threaded ` + build are still subject to reference count contention. It is possible to help + mitigate this problem through deferred reference counting in the C API + (:c:func:`PyUnstable_Object_EnableDeferredRefcount`), but that has the downside + of damaging single-threaded performance. Immortality is able to avoid reference + count contention while also keeping good single-threaded performance. +- Immortality can help memory usage by avoiding copy-on-write operations in + child processes. Instagram has been `doing this in Cinder`_ for years, + and it would be nice to let CPython also take advantage of it. This is + mainly an incidental goal of this PEP. + +.. _doing this in Cinder: https://engineering.fb.com/2023/08/15/developer-tools/immortal-objects-for-python-instagram-meta/ + + +Immortal Objects Must Remain in CPython +*************************************** + +In the past, the main pushback to exposing APIs for immortality is that +it's exposing an implementation detail, and would force CPython to keep +immortality forever. Unfortunately, it's likely that point has already been +reached: too many users, and `CPython itself`_, omit reference counting for +known immortal objects, such as :c:data:`Py_None`. Since there's no good way +to deprecate that, CPython will always need some form of immortality to retain +compatibility. That said, this proposal still keeps all new APIs unstable. + +.. _CPython itself: https://github.com/python/cpython/issues/103906 + +Objects Cannot be Directly Shared Between Interpreters +****************************************************** + +The main benefit of immortality is that it makes it somewhat +possible to share objects between subinterpreters. + +Currently, objects cannot be shared for two main reasons, +which will be discussed further in a moment. As of writing, the rule +is that you cannot modify the reference count of an object created +in a different interpreter, and since you can't modify the reference +count, you can't do anything with the object. Since immortality prevents +reference counting operations, this PEP will solve that issue, allowing +for an object proxy that can safely be used with reference counting APIs +in all interpreters. + +Reference Count Modifications Must be Per-interpreter +----------------------------------------------------- + +On GIL-enabled builds of CPython, reference counting operations are protected +by the interpreter's GIL, meaning that trying to modify the reference count of +an object that belongs to another interpreter could cause data races. + +This isn't a problem on the free-threaded build as reference count operations +are atomic, but subinterpreters are supported on the GIL-enabled build. An +alternative option could be to make reference counting also atomic on the +GIL-enabled build, but that has been shown to damage single-threaded performance +or compromise compatibility with the stable ABI. + +Immortality is a clever way of fixing this problem, because by avoiding +reference counting, there is no risk of data races. In fact, both +subinterpreters and free-threading already use immortality for thread-safety +on some common objects, such as ``None``, ``True``, or ``False``. + +Lastly, immortality is also faster than deferred reference counting +(:c:func:`PyUnstable_Object_EnableDeferredRefcount`), which is used for +mitigating reference count contention on some objects on the free-threaded build. +It would be nice to allow users (likely large C API wrappers) to take advantage +of that speedup. + +Deallocators Rely On the Interpreter State +------------------------------------------ + +Another, less intuitive, danger of reference counting is that when the +object's reference count reaches zero, it immediately tries to get freed. +In doing so, an object will release its memory back to CPython's object allocator +(typically via :c:func:`PyObject_GC_Del` or :c:func:`PyObject_Free`). This +allocator is per-interpreter, so it is unsafe to try to release an object's +memory in an interpreter different from the one that created it. + +Immortality Has No Impact on Reference Counting Performance +----------------------------------------------------------- + +If the primary issue with cross-interpreter reference counting is thread-safety, +why not make reference counting atomic? Unfortunately, atomic operations are +slower than their non-atomic counterparts, so this would hurt overalll +performance. :pep:`703` managed to solve this issue using biased reference +counting, splitting the reference count field into two, but at the cost of +breaking the stable C API. Atomic reference counting on its own also doesn't +solve the per-interpreter deallocator issue as mentioned previously. + +Immortality is a Powerful Tool to Wield +*************************************** + +In general, immortal objects seem to have a lot of applications that aren't +immediately clear to CPython maintainers. For example, `Nanobind used immortal objects`_ +in its original :term:`free threading` implementation. So, it seems like immortality +has interesting use-cases in third-party libraries. + +.. _Nanobind used immortal objects: https://github.com/wjakob/nanobind/pull/695 + +Specification +============= + +Changes to the Internal API +*************************** + +Internally, there are two functions for making an object immortal: +``_Py_SetImmortal`` and ``_Py_SetImmortalUntracked``. +Because the interpreter might want to do its own things with immortal +objects, either now or in the future, the interpreter state should yield +control of a user-defined immortal object to the internal API. So, +the existing internal functions will gain an extra case to remove an object +from the user-defined immortals, so the interpreter won't try and manually +free it later. A new internal function, ``_Py_SetImmortalKnown``, will be +used instead for places where it's certain that objects could not have +been made immortal by a user. + +Changes to String Interning +*************************** + +Currently, the string interning implementation is reliant +on the idea that it controls when a string will become immortal. +Since a user can now immortalize a string on their own, string +interning has to be made "self-aware," meaning that it has to be +able to dynamically figure out if its immortal or not. + +The only place where this needs to change is in +:c:func:`PyUnicode_CHECK_INTERNED`. If the current state is +``SSTATE_INTERNED_MORTAL``, but the object is immortal, then it should +return ``SSTATE_INTERNED_IMMORTAL`` instead. + +Changes to Object Finalization +****************************** + +The C API function, :c:func:`PyObject_CallFinalizer`, must be able to +de-duplicate calls to immortal objects, even if they're non-GC types. + +Immortalization APIs +******************** + +This PEP introduces two new semi-public APIs for making an object immortal: +:c:func:`PyUnstable_Immortalize` and :func:`sys._immortalize`. + +.. c:function:: int PyUnstable_Immortalize(PyObject *obj) + + Make *obj* :term:`immortal`. An :term:`immortal` object can have higher performance for + :term:`reference count` modifications with the cost of being less memory efficient. + + This function returns ``1`` if *obj* was successfully made immortal, or + ``0`` if the object is already immortal. On error, this function returns + ``-1`` with an exception set. The caller must hold an + :term:`attached thread state`. + + In order to be made immortal, an object must follow the "immortalization + contract". This contract is based on best practices in Python's C API, and + it's very unlikely that a type doesn't follow it: + + * The object must be allocated under either the "object" or "memory" allocator + domains. See :ref:`Allocator Domains ` for more information. + Default values of :c:member:`~PyTypeObject.tp_alloc` always use the object + allocator. + * All :term:`strong references ` released in the object's + :c:member:`~PyTypeObject.tp_dealloc` must also be traversed by the + object's :c:member:`~PyTypeObject.tp_traverse` slot, if the type can + contain circular references. This is a general requirement for any garbage + collected type. + * All finalization for an object must be done in + :c:member:`~PyTypeObject.tp_finalize` by + :c:func:`PyObject_CallFinalizerFromDealloc`. + + .. warning:: + Abuse of this function can result in high memory usage for a program. + +.. function:: sys._immortalize(obj) + + Make *obj* an :term:`immortal` object. This means that it will not be deallocated for the + lifetime of the Python interpreter. + + For objects that have costly modifications to their :term:`reference count`, + making them :term:`immortal` can help improve performance. For example, on + the :term:`free-threaded ` build, making an object + :term:`immortal` can help mitigate bottlenecks that come from concurrent + modification of an object's :term:`reference count`. + + .. warning:: + This function should be considered a low-level routine that most users should avoid. Making too many + objects immortal can result in drastically higher memory usage for a program. + +Immortalization Contract +************************ + +The hardest part about immortality is the deallocation, which has to rely +on some assumptions about the object model in order to work properly. We'll +refer to this as the "immortalization contract." These assumptions aren't all +that disruptive, as they're already documented requires and most (if not all) +extensions in the wild already follow these rules. The only difference is that +these are a hard requirement for immortalization, whereas they're sometimes only +"best practices" in the current API. + +Better yet, the immortalization contract only applies to the C API, because objects +defined in Python already follow these rules. Note that even for objects that +don't properly uphold the immortalization contract will not immediately break +if this PEP is accepted; they'll only break if a user decides to immortalize them. +See `Backwards Compatibility`_ for more information. + +Reference Counts Cannot be Relied On +------------------------------------ + +This is already true today, but becomes moreso with this proposal. +Any object that relies on the reference count (typically, in assertions +or in tests) will now break if they are made immortal, because the reference +count of an object will unexpectedly not change. + +In one of the 3.14 alphas, this rule about reference counting was +`brought up`_ for clarification by a user of the +limited C API. From that post, it's clear that reference count numbers are +an implementation detail: + + But honestly, expecting specific refcounts is not going to work in the + future anyway, due to changes in GC implementation and free threading. + +In addition, the :term:`documentation ` explicitly mentions +that reference counts are unstable: + + In CPython, reference counts are not considered to be stable or + well-defined values; the number of references to an object, and how that + number is affected by Python code, may be different between versions. + +.. _brought up: https://discuss.python.org/t/72006 + +Destructors Must Use ``tp_finalize`` +------------------------------------ + +Once again, this should already be true for all extensions. +Since Python 3.4 (:pep:`442`), :c:member:`~PyTypeObject.tp_finalize` alongside +:c:func:`PyObject_CallFinalizerFromDealloc` has been the correct way to run +cleanup code for an extension type. Immortal object deallocation requires that +:c:member:`~PyTypeObject.tp_dealloc` will not try and run Python code, and instead +defer it to :c:member:`~PyTypeObject.tp_finalize`, for both GC and non-GC types. + +As long as :c:func:`PyObject_CallFinalizerFromDealloc` is used in the destructor, +it is completely safe to run cleanup code, because it will just end up no-oping +due to the de-duplication from :c:func:`PyObject_CallFinalizer` (the actual +finalizer call will have been called earlier during immortal object cleanup; +it's just that destruction is one of the last steps in immortal object +deallocation). + +Using the Object or Memory Domain is a Must +------------------------------------------- + +For an object immortal on the heap, the object domain (:c:func:`PyObject_Malloc` +or :c:macro:`PyObject_New`) or the memory domain (:c:func:`PyMem_Malloc`) +must be used. Otherwise, the interpreter will not be able to hijack the +object's allocator and will result in use-after-free violations during +deallocation. Note that in 3.13, using the object domain is already a +:ref:`hard requirement ` for the free threading +garbage collector. + + The free-threaded build requires that only Python objects are allocated + using the “object” domain and that all Python objects are allocated using + that domain. This differs from the prior Python versions, where this was + only a best practice and not a hard requirement. + +Reference Cycles Must be Clearable and Traversable +-------------------------------------------------- + +In order for immortal object deallocation to be safe, an object that can have +reference cycles must be able to clear those reference cycles via +their :c:member:`~PyTypeObject.tp_clear` slot. + +Currently, most extension types that hold a :term:`strong reference` to another +Python object will be able to properly clear references through the garbage +collector. In rare cases, it's possible that a buggy extension type could miss +traversing a strong reference. For immortal object deallocation to work +correctly, Python must be able to identify reference cycles on immortal objects. +Prior to this proposal, reference cycles that aren't traversed by the garbage +collector will just leak, but now if they're immortalized, they +will crash. There's not all that much that can be done about this problem; it's +a bug anyway to have these hidden reference cycles, but it just becomes more of +an issue with this PEP. + +Immortal Object Deallocation +**************************** + +The hardest part of immortalizing arbitrary objects is dealing with +deallocating them. The "intuitive" way of doing it would be to go +through the list of immortal objects, set their reference count to zero, +and then run their deallocator. Unfortunately, immortal objects can contain +circular references, and if that's the case, simply deallocating them in this +way would result in a use-after-free violation. Instead, the immortal object +deallocation process intends to simulate what would happen if the objects were +cleaned up by a garbage collection. In this PEP, this process is divided into +three iterations over the full list of user-defined immortal objects, or "phases". + +Phase One: Finalization +----------------------- + +The initial problem to solve with deallocating immortal objects is that they +can run finalizers, which might reference other immortal objects. Imagine a +case where there are two immortal objects: A, and B. Object B has a finalizer +that references object A, and object A has a finalizer that references object +B. The solution is to simply run their finalizer seperately from their +deallocator. + +Temporary Mortalization for Finalizers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is documented (and expected with assertions) that finalizers are run with +a reference count of 1, so each immortal object needs to be temporarily +mortalized; this can be done with the private ``_Py_SetMortal`` API +that currently exists. After the finalizer is done, it must be immortalized +again so other finalizers don't accidentally deallocate it in their own +finalizers by releasing a reference. + +In addition, an object might expect that its finalizer only be called by the +garbage collector if the type doesn't call +:c:func:`PyObject_CallFinalizerFromDealloc` in its destructor, so objects that +were GC-tracked prior to immortalized immortal should get re-tracked for the +finalizer. + +The psuedo-code looks like this: + +.. code-block:: python + + def finalize_immortals(interp: PyInterpreterState) -> None: + # iter_immortals() is implementation-dependent + for immortal in iter_immortals(interp.runtime_immortals.values): + _Py_SetMortal(immortal.object, 1) + if immortal.gc_tracked: + PyObject_GC_Track(immortal.object) + + PyObject_CallFinalizer(immortal.object) + + if immortal.gc_tracked: + PyObject_GC_UnTrack(immortal.object) + _Py_SetImmortalKnown(immortal.object) + +Identifying Immortal Referents +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To find the mortal objects that might want to reference an immortal, we have +to make a reasonable assumption about how garbage collection works: in order +for an object to :c:func:`Py_DECREF` another object in its destructor, it +must hold a :term:`strong reference` to that object, and therefore traverse +it in its :c:member:`~PyTypeObject.tp_traverse` slot. With that in mind, Python +supports customizing the ``visitproc`` passed to the ``tp_traverse`` function, so +it's possible to find all the mortal objects referenced by an immortal and +correctly finalize them. + +While unlikely, it's possible that some types rely on being garbage collector +tracked in their ``tp_traverse`` slot, because it's typically only possible to +be called during scanning. To ensure that things don't accidentally break, immortal +objects are once again re-tracked to traverse the object. + +Since finalizers might mutate the references of other objects, the interpreter must +continuously execute finalizers until it traverses (and calls +:c:func:`PyObject_CallFinalizer`) over all the referents without running a +finalizer. + +Possible psuedo-code: + +.. code-block:: python + + def visit_finalize(op: object, traversing: MutableSet[object]) -> int: + if id(op) in traversing: + return 0 + + traversing.add(id(op)) + PyObject_CallFinalizer(op) + was_called: int = finalizer_was_called(op) + + if _PyObject_IS_GC(op) and Py_TYPE(op).tp_traverse is not None: + if Py_TYPE(op).tp_traverse(op, visit_finalize, traversing) == 1: + return 1 + + return was_called + + + def finalize_immortal_referents() -> None: + traversing = set[int]() + ran_finalizer: bool = True + + while ran_finalizer: + ran_finalizer = False + + for immortal in iter_immortals(): + op: object = immortal.object + if immortal.gc_tracked and Py_TYPE(op).traverse is not None: + PyObject_GC_Track(op) + + if Py_TYPE(op).traverse(op, visit_finalize, traversing) and did_run_finalizer(): + ran_finalizer = True + + PyObject_GC_UnTrack(op) + + +Phase Two: Clearing and Destruction +----------------------------------- + +At this point, the second phase assumes that executing the +:c:member:`~PyTypeObject.tp_clear` or :c:member:`PyTypeObject.tp_dealloc` +of an immortal object doesn't run Python code, because all possible +:term:`strong references ` that be released should have +already been traversed and finalized by phase one. The interpreter may need +to finalize immortal referents again before running phase two to make sure +that this is true. + +Phase two should typically happen after all other objects in the interpreter +have been cleaned up. It is divided into two iterations: one to run the +``tp_clear`` slot if the immortal was originally GC-tracked, and the +``tp_dealloc`` in the second iteration. + +After phase two, the interpreter should not attempt to use an object +(other than for reference counting) that had been exposed to the user +in some way, because it could have been made immortal and is now mostly +deallocated (the pure object structures are still alive in memory, but all +remaining memory on an object has been freed). + +Deferred Memory Deletion +^^^^^^^^^^^^^^^^^^^^^^^^ + +Objects in Python are supposed to be under the "object" domain. Again, this is +already required on free-threaded builds. It's generally safe enough to assume +that all objects are using the object allocator, but this PEP also permits +use of the "memory" domain. See `Using the Object or Memory Domain is a Must`_. + +Python's allocators can be customized via :c:func:`PyMem_SetAllocator`, which +makes it possible for the interpreter to change how objects free their memory. +For immortal object deallocation, the interpreter needs to temporarily suspend +``free()`` operations on objects so reference cycles won't break things +This algorithm will be known as "deferred memory deletion". + +When deferred memory deletion is enabled, calling :c:func:`PyMem_Free`, +:c:func:`PyObject_Free`, or :c:func:`PyObject_GC_Del` will not free the +passed pointer, but instead store the pointer in a deferred memory bank +to be deleted later. The pointer will be kept as completely valid memory +until the deferred memory bank is cleared. + +Some objects also use internal freelist APIs instead of usual deallocators, +so those have to be disabled during deferred memory deallocation. Python's +internal freelists are cleared and stored in the deferred memory bank to be +restored later. + +After deferred memory deletion has been disabled and the pointers stored in +the deferred memory bank have been deleted, the interpreter must not try +to modify the reference count of any object visible to a user, as it could +have been made immortal and is now deleted. As such, the deferred memory bank +should be cleared at the very end of an interpreter's life. + +Preventing Reference Cycle UAFs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Before any destruction of immortal objects starts to happen, +deferred memory deletion needs to be enabled. +This is because the garbage collector doesn't manually call +:c:member:`~PyTypeObject.tp_dealloc`, but instead it relies on +:c:member:`~PyTypeObject.tp_clear` to get reference counts to zero. + +Unfortunately, the interpreter does not know the "real" number of references to +immortal objects (because there is no real reference count for an immortal +object), so deleting an immortal object arbitrarily could cause use-after-free +violations if immortal objects reference one another. +Deferred memory deletion will prevent this, because an immortal object's +reference count field will remain in valid memory and thus can still +be reference counted (but *not* used for any other purpose; that's why it was +finalized beforehand). + +For destruction, each immortal object is mortalized (to a reference count of +zero), and then their ``tp_dealloc`` slot is triggered. Once again, deferred +memory deletion will prevent the object structure from getting freed, so it's +safe to reference count an immortal object postmortem. + +After an immortal object's destructor is run, it will be immortalized once more +in case other destructors try to release a reference to it. +The immortal objects will remain with an immortal reference count for the rest +of the interpreter, and will only be finally deleted in the third and final +phase of immortal object deallocation. + +Psuedo-code for phase two: + +.. code-block:: python + + def destruct_immortals() -> None: + enable_deferred_memory_deletion() + for immortal in iter_immortals(): + op: object = immortal.object + + if immortal.gc_tracked: + PyObject_GC_Track(op) + + if immortal.gc_tracked and Py_TYPE(op).clear is not None: + Py_TYPE(op).clear(op) + + for immortal in iter_immortals(interp.runtime_immortals.values): + op: object = immortal.object + + _Py_SetMortal(op, 1) + Py_DECREF(op) + _Py_SetImmortalKnown(op) + + disable_deferred_memory_deletion() + +Phase Three: Deletion +--------------------- + +Deletion is the simplest phase of immortal object deallocation. +The deferred memory is deleted and the immortal objects have been finally +released. Once again, after this phase, it is unsafe for the interpreter to +reference count any object that could have been exposed to the user, +because if it was immortal, it will now be freed. + +The psuedo-code is simple: + +.. code-block:: python + + def delete_immortals() -> None: + delete_deferred_memory() + +Backwards Compatibility +======================= + +The `Immortalization Contract`_ is very slightly backwards-incompatible. +The contract is based on best practices in the C API; it doesn't have +anything that's really new, it's just based around what objects currently +do. That being said, there was nothing previously enforcing that objects follow +these assumptions, but the authors of this PEP have never seen code in the +wild that breaks the immortalization contract. + +In addition, this PEP only exposes an opt-in feature; types that don't support +immortalization will continue to work as long as they are not immortalized. + +What if CPython Removes Immortality? +************************************ + +The APIs in this PEP are exposed as unstable, allowing CPython to +remove them later on if it needs to. + +Security Implications +===================== + +The main issue with immortality is that, well, the objects are immortal. +They don't get deallocated, at least not until the interpreter is shutting +down. That means for applications that make a lot of things immortal will +have higher memory usage. Even worse, applications that don't ever shut down +(such as web servers) will essentially leak the objects, so making something +immortal can possibly be an attack vector depending on where it's used. + +How to Teach This +================= + +:c:func:`PyUnstable_Immortalize` will be documented in the C API documentation +and :func:`sys._immortalize` in the :mod:`sys` documentation. They will both +contain warnings about the increased memory usage that results from immortalizing +too many objects. + +Reference Implementation +======================== + +A reference implementation of this PEP can be found +`here `_. + +Rejected Ideas +============== + +This PEP currently has no rejected ideas. + +Open Issues +=========== + +This PEP currently has no open issues. + +Copyright +========= + +This document is placed in the public domain or under the +CC0-1.0-Universal license, whichever is more permissive.