Thanks, Victor for awesome PEP I am a big +1 on this proposal since some of the core developers already need the evolution of C API. I believe this proposal is not only for alternative python compiler implementation but also gives a chance for enhancing CPython performance. And I love this proposal since the suggestion does not say break everything in a one-shot but pursuing incremental change.
The one thing that we have to do for this PEP is that informing the change very well to the 3rd party library. With well-written changing documentation, I believe most of the impactful 3rd party library which sustains Python library community will have enough time for preparing change. I believe that if there is no change there will be no evolution. Let's make CPython more fastly with temporarily suffering Regards from Korea, Dong-hee 2020년 6월 22일 (월) 오후 9:13, Victor Stinner <vstin...@python.org>님이 작성: > > Hi, > > PEP available at: https://www.python.org/dev/peps/pep-0620/ > > <introduction> > This PEP is the result of 4 years of research work on the C API: > https://pythoncapi.readthedocs.io/ > > It's the third version. The first version (2017) proposed to add a > "new C API" and advised C extensions maintainers to opt-in for it: it > was basically the same idea as PEP 384 limited C API but in a > different color. Well, I had no idea of what I was doing :-) The > second version (April 2020) proposed to add a new Python runtime built > from the same code base as the regular Python runtime but in a > different build mode, the regular Python would continue to be fully > compatible. > > I wrote the third version, the PEP 620, from scratch. It now gives an > explicit and concrete list of incompatible C API changes, and has > better motivation and rationale sections. The main PEP novelty is the > new pythoncapi_compat.h header file distributed with Python to provide > new C API functions to old Python versions, the second novelty is the > process to reduce the number of broken C extensions. > > Whereas PEPs are usually implemented in a single Python version, the > implementation of this PEP is expected to be done carefully over > multiple Python versions. The PEP lists many changes which are already > implemented in Python 3.7, 3.8 and 3.9. It defines a process to reduce > the number of broken C extensions when introducing the incompatible C > API changes listed in the PEP. The process dictates the rhythm of > these changes. > </introduction> > > > PEP: 620 > Title: Hide implementation details from the C API > Author: Victor Stinner <vstin...@python.org> > Status: Draft > Type: Standards Track > Content-Type: text/x-rst > Created: 19-June-2020 > Python-Version: 3.10 > > Abstract > ======== > > Introduce C API incompatible changes to hide implementation details. > > Once most implementation details will be hidden, evolution of CPython > internals would be less limited by C API backward compatibility issues. > It will be way easier to add new features. > > It becomes possible to experiment with more advanced optimizations in CPython > than just micro-optimizations, like tagged pointers. > > Define a process to reduce the number of broken C extensions. > > The implementation of this PEP is expected to be done carefully over > multiple Python versions. It already started in Python 3.7 and most > changes are already completed. The `Process to reduce the number of > broken C extensions`_ dictates the rhythm. > > > Motivation > ========== > > The C API blocks CPython evolutions > ----------------------------------- > > Adding or removing members of C structures is causing multiple backward > compatibility issues. > > Adding a new member breaks the stable ABI (PEP 384), especially for > types declared statically (e.g. ``static PyTypeObject MyType = > {...};``). In Python 3.4, the PEP 442 "Safe object finalization" added > the ``tp_finalize`` member at the end of the ``PyTypeObject`` structure. > For ABI backward compatibility, a new ``Py_TPFLAGS_HAVE_FINALIZE`` type > flag was required to announce if the type structure contains the > ``tp_finalize`` member. The flag was removed in Python 3.8 (`bpo-32388 > <https://bugs.python.org/issue32388>`_). > > The ``PyTypeObject.tp_print`` member, deprecated since Python 3.0 > released in 2009, has been removed in the Python 3.8 development cycle. > But the change broke too many C extensions and had to be reverted before > 3.8 final release. Finally, the member was removed again in Python 3.9. > > C extensions rely on the ability to access directly structure members, > indirectly through the C API, or even directly. Modifying structures > like ``PyListObject`` cannot be even considered. > > The ``PyTypeObject`` structure is the one which evolved the most, simply > because there was no other way to evolve CPython than modifying it. > > In the C API, all Python objects are passed as ``PyObject*``: a pointer > to a ``PyObject`` structure. Experimenting tagged pointers in CPython is > blocked by the fact that a C extension can technically dereference a > ``PyObject*`` pointer and access ``PyObject`` members. Small "objects" > can be stored as a tagged pointer with no concrete ``PyObject`` > structure. > > Replacing Python garbage collector with a tracing garbage collector > would also need to remove ``PyObject.ob_refcnt`` reference counter, > whereas currently ``Py_INCREF()`` and ``Py_DECREF()`` macros access > directly to ``PyObject.ob_refcnt``. > > Same CPython design since 1990: structures and reference counting > ----------------------------------------------------------------- > > When the CPython project was created, it was written with one principle: > keep the implementation simple enough so it can be maintained by a > single developer. CPython complexity grew a lot and many > micro-optimizations have been implemented, but CPython core design has > not changed. > > Members of ``PyObject`` and ``PyTupleObject`` structures have not > changed since the "Initial revision" commit (1990):: > > #define OB_HEAD \ > unsigned int ob_refcnt; \ > struct _typeobject *ob_type; > > typedef struct _object { > OB_HEAD > } object; > > typedef struct { > OB_VARHEAD > object *ob_item[1]; > } tupleobject; > > Only names changed: ``object`` was renamed to ``PyObject`` and > ``tupleobject`` was renamed to ``PyTupleObject``. > > CPython still tracks Python objects lifetime using reference counting > internally and for third party C extensions (through the Python C API). > > All Python objects must be allocated on the heap and cannot be moved. > > Why is PyPy more efficient than CPython? > ---------------------------------------- > > The PyPy project is a Python implementation which is 4.2x faster than > CPython on average. PyPy developers chose to not fork CPython, but start > from scratch to have more freedom in terms of optimization choices. > > PyPy does not use reference counting, but a tracing garbage collector > which moves objects. Objects can be allocated on the stack (or even not > at all), rather than always having to be allocated on the heap. > > Objects layouts are designed with performance in mind. For example, a > list strategy stores integers directly as integers, rather than objects. > > Moreover, PyPy also has a JIT compiler which emits fast code thanks to > the efficient PyPy design. > > PyPy bottleneck: the Python C API > --------------------------------- > > While PyPy is way more efficient than CPython to run pure Python code, > it is as efficient or slower than CPython to run C extensions. > > Since the C API requires ``PyObject*`` and allows to access directly > structure members, PyPy has to associate a CPython object to PyPy > objects and maintain both consistent. Converting a PyPy object to a > CPython object is inefficient. Moreover, reference counting also has to > be implemented on top of PyPy tracing garbage collector. > > These conversions are required because the Python C API is too close to > the CPython implementation: there is no high-level abstraction. > For example, structures members are part of the public C API and nothing > prevents a C extension to get or set directly > ``PyTupleObject.ob_item[0]`` (the first item of a tuple). > > See `Inside cpyext: Why emulating CPython C API is so Hard > <https://morepypy.blogspot.com/2018/09/inside-cpyext-why-emulating-cpython-c.html>`_ > (Sept 2018) by Antonio Cuni for more details. > > > Rationale > ========= > > Hide implementation details > --------------------------- > > Hiding implementation details from the C API has multiple advantages: > > * It becomes possible to experiment with more advanced optimizations in > CPython than just micro-optimizations. For example, tagged pointers, > and replace the garbage collector with a tracing garbage collector > which can move objects. > * Adding new features in CPython becomes easier. > * PyPy should be able to avoid conversions to CPython objects in more > cases: keep efficient PyPy objects. > * It becomes easier to implement the C API for a new Python > implementation. > * More C extensions will be compatible with Python implementations other > than CPython. > > Relationship with the limited C API > ----------------------------------- > > The PEP 384 "Defining a Stable ABI" is in Python 3.4. It introduces the > "limited C API": a subset of the C API. When the limited C API is used, > it becomes possible to build a C extension only once and use it on > multiple Python versions: that's the stable ABI. > > The main limitation of the PEP 384 is that C extensions have to opt-in > for the limited C API. Only very few projects made this choice, > usually to ease distribution of binaries, especially on Windows. > > This PEP moves the C API towards the limited C API. > > Ideally, the C API will become the limited C API and all C extensions > will use the stable ABI, but this is out of this PEP scope. > > > Specification > ============= > > Summary > ------- > > * (**Completed**) Reorganize the C API header files: create > ``Include/cpython/`` and > ``Include/internal/`` subdirectories. > * (**Completed**) Move private functions exposing implementation > details to the internal > C API. > * (**Completed**) Convert macros to static inline functions. > * (**Completed**) Add new functions ``Py_SET_TYPE()``, ``Py_SET_REFCNT()`` and > ``Py_SET_SIZE()``. The ``Py_TYPE()``, ``Py_REFCNT()`` and > ``Py_SIZE()`` macros become functions which cannot be used as l-value. > * (**Completed**) New C API functions must not return borrowed > references. > * (**In Progress**) Provide ``pythoncapi_compat.h`` header file. > * (**In Progress**) Make structures opaque, add getter and setter > functions. > * (**Not Started**) Deprecate ``PySequence_Fast_ITEMS()``. > * (**Not Started**) Convert ``PyTuple_GET_ITEM()`` and > ``PyList_GET_ITEM()`` macros to static inline functions. > > Reorganize the C API header files > --------------------------------- > > The first consumer of the C API was Python itself. There is no clear > separation between APIs which must not be used outside Python, and API > which are public on purpose. > > Header files must be reorganized in 3 API: > > * ``Include/`` directory is the limited C API: no implementation > details, structures are opaque. C extensions using it get a stable > ABI. > * ``Include/cpython/`` directory is the CPython C API: less "portable" > API, depends more on the Python version, expose some implementation > details, few incompatible changes can happen. > * ``Internal/internal/`` directory is the internal C API: implementation > details, incompatible changes are likely at each Python release. > > The creation of the ``Include/cpython/`` directory is fully backward > compatible. ``Include/cpython/`` header files cannot be included > directly and are included automatically by ``Include/`` header files > when the ``Py_LIMITED_API`` macro is not defined. > > The internal C API is installed and can be used for specific usage like > debuggers and profilers which must access structures members without > executing code. C extensions using the internal C API are tightly > coupled to a Python version and must be recompiled at each Python > version. > > **STATUS**: Completed (in Python 3.8) > > The reorganization of header files started in Python 3.7 and was > completed in Python 3.8: > > * `bpo-35134 <https://bugs.python.org/issue35134>`_: Add a new > Include/cpython/ subdirectory for the "CPython API" with > implementation details. > * `bpo-35081 <https://bugs.python.org/issue35081>`_: Move internal > headers to ``Include/internal/`` > > Move private functions to the internal C API > -------------------------------------------- > > Private functions which expose implementation details must be moved to > the internal C API. > > If a C extension relies on a CPython private function which exposes > CPython implementation details, other Python implementations have to > re-implement this private function to support this C extension. > > **STATUS**: Completed (in Python 3.9) > > Private functions moved to the internal C API in Python 3.8: > > * ``_PyObject_GC_TRACK()``, ``_PyObject_GC_UNTRACK()`` > > Macros and functions excluded from the limited C API in Python 3.9: > > * ``_PyObject_SIZE()``, ``_PyObject_VAR_SIZE()`` > * ``PyThreadState_DeleteCurrent()`` > * ``PyFPE_START_PROTECT()``, ``PyFPE_END_PROTECT()`` > * ``_Py_NewReference()``, ``_Py_ForgetReference()`` > * ``_PyTraceMalloc_NewReference()`` > * ``_Py_GetRefTotal()`` > > Private functions moved to the internal C API in Python 3.9: > > * GC functions like ``_Py_AS_GC()``, ``_PyObject_GC_IS_TRACKED()`` > and ``_PyGCHead_NEXT()`` > * ``_Py_AddToAllObjects()`` (not exported) > * ``_PyDebug_PrintTotalRefs()``, ``_Py_PrintReferences()``, > ``_Py_PrintReferenceAddresses()`` (not exported) > > Public "clear free list" functions moved to the internal C API an > renamed to private functions and in Python 3.9: > > * ``PyAsyncGen_ClearFreeLists()`` > * ``PyContext_ClearFreeList()`` > * ``PyDict_ClearFreeList()`` > * ``PyFloat_ClearFreeList()`` > * ``PyFrame_ClearFreeList()`` > * ``PyList_ClearFreeList()`` > * ``PyTuple_ClearFreeList()`` > * Functions simply removed: > > * ``PyMethod_ClearFreeList()`` and ``PyCFunction_ClearFreeList()``: > bound method free list removed in Python 3.9. > * ``PySet_ClearFreeList()``: set free list removed in Python 3.4. > * ``PyUnicode_ClearFreeList()``: Unicode free list removed > in Python 3.3. > > Convert macros to static inline functions > ----------------------------------------- > > Converting macros to static inline functions have multiple advantages: > > * Functions have well defined parameter types and return type. > * Functions can use variables with a well defined scope (the function). > * Debugger can be put breakpoints on functions and profilers can display > the function name in the call stacks. In most cases, it works even > when a static inline function is inlined. > * Functions don't have `macros pitfalls > <https://gcc.gnu.org/onlinedocs/cpp/Macro-Pitfalls.html>`_. > > Converting macros to static inline functions should only impact very few > C extensions which use macros in unusual ways. > > For backward compatibility, functions must continue to accept any type, > not only ``PyObject*``, to avoid compiler warnings, since most macros > cast their parameters to ``PyObject*``. > > Python 3.6 requires C compilers to support static inline functions: the > PEP 7 requires a subset of C99. > > **STATUS**: Completed (in Python 3.9) > > Macros converted to static inline functions in Python 3.8: > > * ``Py_INCREF()``, ``Py_DECREF()`` > * ``Py_XINCREF()``, ``Py_XDECREF()`` > * ``PyObject_INIT()``, ``PyObject_INIT_VAR()`` > * ``_PyObject_GC_TRACK()``, ``_PyObject_GC_UNTRACK()``, ``_Py_Dealloc()`` > > Macros converted to regular functions in Python 3.9: > > * ``Py_EnterRecursiveCall()``, ``Py_LeaveRecursiveCall()`` > (added to the limited C API) > * ``PyObject_INIT()``, ``PyObject_INIT_VAR()`` > * ``PyObject_GET_WEAKREFS_LISTPTR()`` > * ``PyObject_CheckBuffer()`` > * ``PyIndex_Check()`` > * ``PyObject_IS_GC()`` > * ``PyObject_NEW()`` (alias to ``PyObject_New()``), > ``PyObject_NEW_VAR()`` (alias to ``PyObject_NewVar()``) > * ``PyType_HasFeature()`` (always call ``PyType_GetFlags()``) > * ``Py_TRASHCAN_BEGIN_CONDITION()`` and ``Py_TRASHCAN_END()`` macros > now call functions which hide implementation details, rather than > accessing directly members of the ``PyThreadState`` structure. > > Make structures opaque > ---------------------- > > All structures of the C API should become opaque: C extensions must > use getter or setter functions to get or set structure members. For > example, ``tuple->ob_item[0]`` must be replaced with > ``PyTuple_GET_ITEM(tuple, 0)``. > > To be able to move away from reference counting, ``PyObject`` must > become opaque. Currently, the reference counter ``PyObject.ob_refcnt`` > is exposed in the C API. All structures must become opaque, since they > "inherit" from PyObject. For, ``PyFloatObject`` inherits from > ``PyObject``:: > > typedef struct { > PyObject ob_base; > double ob_fval; > } PyFloatObject; > > Making ``PyObject`` fully opaque requires converting ``Py_INCREF()`` and > ``Py_DECREF()`` macros to function calls. This change has an impact on > performance. It is likely to be one of the very last changes when making > structures opaque. > > Making ``PyTypeObject`` structure opaque breaks C extensions declaring > types statically (e.g. ``static PyTypeObject MyType = {...};``). C > extensions must use ``PyType_FromSpec()`` to allocate types on the heap > instead. Using heap types has other advantages like being compatible > with subinterpreters. Combined with PEP 489 "Multi-phase extension > module initialization", it makes a C extension behavior closer to a > Python module, like allowing to create more than one module instance. > > Making ``PyThreadState`` structure opaque requires adding getter and > setter functions for members used by C extensions. > > **STATUS**: In Progress (started in Python 3.8) > > The ``PyInterpreterState`` structure was made opaque in Python 3.8 > (`bpo-35886 <https://bugs.python.org/issue35886>`_) and the > ``PyGC_Head`` structure (`bpo-40241 > <https://bugs.python.org/issue40241>`_) was made opaque in Python 3.9. > > Issues tracking the work to prepare the C API to make following > structures opaque: > > * ``PyObject``: `bpo-39573 <https://bugs.python.org/issue39573>`_ > * ``PyTypeObject``: `bpo-40170 <https://bugs.python.org/issue40170>`_ > * ``PyFrameObject``: `bpo-40421 <https://bugs.python.org/issue40421>`_ > > * Python 3.9 adds ``PyFrame_GetCode()`` and ``PyFrame_GetBack()`` > getter functions, and moves ``PyFrame_GetLineNumber`` to the limited > C API. > > * ``PyThreadState``: `bpo-39947 <https://bugs.python.org/issue39947>`_ > > * Python 3.9 adds 3 getter functions: ``PyThreadState_GetFrame()``, > ``PyThreadState_GetID()``, ``PyThreadState_GetInterpreter()``. > > Disallow using Py_TYPE() as l-value > ----------------------------------- > > The ``Py_TYPE()`` function gets an object type, its ``PyObject.ob_type`` > member. It is implemented as a macro which can be used as an l-value to > set the type: ``Py_TYPE(obj) = new_type``. This code relies on the > assumption that ``PyObject.ob_type`` can be modified directly. It > prevents making the ``PyObject`` structure opaque. > > New setter functions ``Py_SET_TYPE()``, ``Py_SET_REFCNT()`` and > ``Py_SET_SIZE()`` are added and must be used instead. > > The ``Py_TYPE()``, ``Py_REFCNT()`` and ``Py_SIZE()`` macros must be > converted to static inline functions which can not be used as l-value. > > For example, the ``Py_TYPE()`` macro:: > > #define Py_TYPE(ob) (((PyObject*)(ob))->ob_type) > > becomes:: > > #define _PyObject_CAST_CONST(op) ((const PyObject*)(op)) > > static inline PyTypeObject* _Py_TYPE(const PyObject *ob) { > return ob->ob_type; > } > > #define Py_TYPE(ob) _Py_TYPE(_PyObject_CAST_CONST(ob)) > > **STATUS**: Completed (in Python 3.10) > > New functions ``Py_SET_TYPE()``, ``Py_SET_REFCNT()`` and > ``Py_SET_SIZE()`` were added to Python 3.9. > > In Python 3.10, ``Py_TYPE()``, ``Py_REFCNT()`` and ``Py_SIZE()`` can no > longer be used as l-value and the new setter functions must be used > instead. > > New C API functions must not return borrowed references > ------------------------------------------------------- > > When a function returns a borrowed reference, Python cannot track when > the caller stops using this reference. > > For example, if the Python ``list`` type is specialized for small > integers, store directly "raw" numbers rather than Python objects, > ``PyList_GetItem()`` has to create a temporary Python object. The > problem is to decide when it is safe to delete the temporary object. > > The general guidelines is to avoid returning borrowed references for new > C API functions. > > No function returning borrowed functions is scheduled for removal by > this PEP. > > **STATUS**: Completed (in Python 3.9) > > In Python 3.9, new C API functions returning Python objects only return > strong references: > > * ``PyFrame_GetBack()`` > * ``PyFrame_GetCode()`` > * ``PyObject_CallNoArgs()`` > * ``PyObject_CallOneArg()`` > * ``PyThreadState_GetFrame()`` > > Avoid functions returning PyObject** > ------------------------------------ > > The ``PySequence_Fast_ITEMS()`` function gives a direct access to an > array of ``PyObject*`` objects. The function is deprecated in favor of > ``PyTuple_GetItem()`` and ``PyList_GetItem()``. > > ``PyTuple_GET_ITEM()`` can be abused to access directly the > ``PyTupleObject.ob_item`` member:: > > PyObject **items = &PyTuple_GET_ITEM(0); > > The ``PyTuple_GET_ITEM()`` and ``PyList_GET_ITEM()`` macros are > converted to static inline functions to disallow that. > > **STATUS**: Not Started > > New pythoncapi_compat.h header file > ----------------------------------- > > Making structures opaque requires modifying C extensions to > use getter and setter functions. The practical issue is how to keep > support for old Python versions which don't have these functions. > > For example, in Python 3.10, it is no longer possible to use > ``Py_TYPE()`` as an l-value. The new ``Py_SET_TYPE()`` function must be > used instead:: > > #if PY_VERSION_HEX >= 0x030900A4 > Py_SET_TYPE(&MyType, &PyType_Type); > #else > Py_TYPE(&MyType) = &PyType_Type; > #endif > > This code may ring a bell to developers who ported their Python code > base from Python 2 to Python 3. > > Python will distribute a new ``pythoncapi_compat.h`` header file which > provides new C API functions to old Python versions. Example:: > > #if PY_VERSION_HEX < 0x030900A4 > static inline void > _Py_SET_TYPE(PyObject *ob, PyTypeObject *type) > { > ob->ob_type = type; > } > #define Py_SET_TYPE(ob, type) _Py_SET_TYPE((PyObject*)(ob), type) > #endif // PY_VERSION_HEX < 0x030900A4 > > Using this header file, ``Py_SET_TYPE()`` can be used on old Python > versions as well. > > Developers can copy this file in their project, or even to only > copy/paste the few functions needed by their C extension. > > **STATUS**: In Progress (implemented but not distributed by CPython yet) > > The ``pythoncapi_compat.h`` header file is currently developer at: > https://github.com/pythoncapi/pythoncapi_compat > > Process to reduce the number of broken C extensions > =================================================== > > Process to reduce the number of broken C extensions when introducing C > API incompatible changes listed in this PEP: > > * Estimate how many popular C extensions are affected by the > incompatible change. > * Coordinate with maintainers of broken C extensions to prepare their > code for the future incompatible change. > * Introduce the incompatible changes in Python. The documentation must > explain how to port existing code. It is recommended to merge such > changes at the beginning of a development cycle to have more time for > tests. > * Changes which are the most likely to break a large number of C > extensions should be announced on the capi-sig mailing list to notify > C extensions maintainers to prepare their project for the next Python. > * If the change breaks too many projects, reverting the change should be > discussed, taking in account the number of broken packages, their > importance in the Python community, and the importance of the change. > > The coordination usually means reporting issues to the projects, or even > proposing changes. It does not require waiting for a new release including > fixes for every broken project. > > Since more and more C extensions are written using Cython, rather > directly using the C API, it is important to ensure that Cython is > prepared in advance for incompatible changes. It gives more time for C > extension maintainers to release a new version with code generated with > the updated Cython (for C extensions distributing the code generated by > Cython). > > Future incompatible changes can be announced by deprecating a function > in the documentation and by annotating the function with > ``Py_DEPRECATED()``. But making a structure opaque and preventing the > usage of a macro as l-value cannot be deprecated with > ``Py_DEPRECATED()``. > > The important part is coordination and finding a balance between CPython > evolutions and backward compatibility. For example, breaking a random, > old, obscure and unmaintained C extension on PyPI is less severe than > breaking numpy. > > If a change is reverted, we move back to the coordination step to better > prepare the change. Once more C extensions are ready, the incompatible > change can be reconsidered. > > > Version History > =============== > > * Version 3, June 2020: PEP rewritten from scratch. Python now > distributes a new ``pythoncapi_compat.h`` header and a process is > defined to reduce the number of broken C extensions when introducing C > API incompatible changes listed in this PEP. > * Version 2, April 2020: > `PEP: Modify the C API to hide implementation details > > <https://mail.python.org/archives/list/python-dev@python.org/thread/HKM774XKU7DPJNLUTYHUB5U6VR6EQMJF/#TKHNENOXP6H34E73XGFOL2KKXSM4Z6T2>`_. > * Version 1, July 2017: > `PEP: Hide implementation details in the C API > > <https://mail.python.org/archives/list/python-id...@python.org/thread/6XATDGWK4VBUQPRHCRLKQECTJIPBVNJQ/#HFBGCWVLSM47JEP6SO67MRFT7Y3EOC44>`_ > sent to python-ideas > > > Copyright > ========= > > This document has been placed in the public domain. > > -- > Night gathers, and now my watch begins. It shall not end until my death. > _______________________________________________ > Python-Dev mailing list -- python-dev@python.org > To unsubscribe send an email to python-dev-le...@python.org > https://mail.python.org/mailman3/lists/python-dev.python.org/ > Message archived at > https://mail.python.org/archives/list/python-dev@python.org/message/EV7F7Z6PLPWJU7SD2UPFEYKYUWU4ZJXZ/ > Code of Conduct: http://python.org/psf/codeofconduct/ -- Software Development Engineer at Kakao corp. Tel: +82 010-3353-9127 Email: donghee.n...@gmail.com | denn...@kakaocorp.com Linkedin: https://www.linkedin.com/in/dong-hee-na-2b713b49/ _______________________________________________ Python-Dev mailing list -- python-dev@python.org To unsubscribe send an email to python-dev-le...@python.org https://mail.python.org/mailman3/lists/python-dev.python.org/ Message archived at https://mail.python.org/archives/list/python-dev@python.org/message/4XU6YHC6K47VURWG3I2QXLZSNPGFSRNL/ Code of Conduct: http://python.org/psf/codeofconduct/