C API: My plan to clarify private vs public functions in Python 3.13

I declare that the Python 3.13 season of “removing as many private C API as possible” ended! I stop here until Python 3.14. I closed the Remove private C API functions (move them to the internal C API) issue.

I will now focus on testing as many C extensions as possible on Python 3.13, see which functions are missing in the public C API and consider making them public (add doc, tests, replace _Py prefix with Py).

And as I wrote, I consider moving back some _Py private functions from the internal C API to the public C API (from Include/internal/ to Include/cpython/) if I don’t have enough time to fix enough C extensions. Especially functions which affect most C extensions. The exact list of affected C extensions and affected functions has to be created.

I plan to continue the work in new issues and only keep this issue to point to new issues.

In the current main branch (Python 3.13), there are 86 private functions (in Include/cpython/) exported with PyAPI_FUNC(). IMO 86 private functions is way better than the 385 private functions exported by Python 3.12. It’s easier to manage.

Some of them can be moved to the internal C API, but the remaining ones are the most complicated to move for various reasons.

For example, in 2020 I failed to remove _Py_NewReference() since it’s used by 3rd party C extensions (and by Python itself, by the way) to implement nice free list optimizations. There is no good replacement for that, and designing a public API to implement a free list is not trivial. Such API stays in the gray area: it should not be used, but I will not blame you if you continue using it.

I didn’t count internal functions (Include/internal/), I don’t care about these ones. See the complete statistics of the C API.

Using the internal C API is fine, but in exchange you are on your own. There is no documentation, usually the API is not tested, no or incomplete error checking, etc. Most of the internal C API remains usable outside CPython itself because they are usages for that, like debuggers and profilers which need to inspect Python internals without modifying its state. Usually, it means reading memory without calling functions (if possible).