Skip to content

Add PyTuple_FromPair #145247

Description

@markshannon

Feature or enhancement

Proposal:

Rationale

We have had quite a few PRs lately suggesting replacing PyTuple_New (which requires manually setting the items) with PyTuple_FromArray.
Many of them have been rejected because of (maybe excessive) requirements for the authors to do benchmarking, even though the code is IMO a clear improvement. Almost all of these have been for pairs.
I propose adding PyTuple_FromPair to provide an idiomatic, fast and safe way to create 2-tuples.
We should also add a "steal" variant as well to simplify replacing PyTuple_New() ... PyTuple_SET_ITEM.

Note: I originally wanted to call this PyTuple_MakePair, but I'm taking PyTuple_FromPair from capi-workgroup/decisions#84 (comment)

Here's all the relevant PRs I could easily find:
#144529
#144531
#144532
#144760
#144771
#144772
#144773
#144829

Prior abandoned issue:
#140052 which also proposes adding PyTuple_FromSingle and which links to capi-workgroup/decisions#84.
capi-workgroup/decisions#84 rejects the idea without any clear rationale.

Clarity

t = PyTuple_New(2);
// error handling here 

// Care must be taken not to leak the invalid tuple
PyTuple_SET_ITEM(t, a);
PyTuple_SET_ITEM(t, b);

compare with

t = PyTuple_FromPair(a, b);
// error handling here

Performance

Compared to PyTuple_New

PyTuple_FromPair can allocate directly from the free list without additional checks. It does not need to check for weird sizes, or special case size zero. It does not need to NULL out the items before they are set. It can determine whether the tuple needs to be tracked or not at construction without the overhead of tracking it, then later untracking it.

Compared to PyTuple_FromArray

The advantage is less than for PyTuple_New, but still significant, as it reduces memory traffic by not storing the items in an array, then reading them out again.

Safety

This has the same safety advantages as PyTuple_FromArray, as it never produces an incomplete tuple. Since much of the code it will replace is using PyTuple_New, this is a worthwhile improvement

Implementation

We should make this a private API to start with. Once it proves it use, then we can make it public.

PyObject *_PyTuple_MakePair(PyObject *a, PyObject *b);

PyObject *_PyTuple_MakePairSteal(PyObject *a, PyObject *b);

Linked PRs

Activity

  1. added
    type-featureA feature request or enhancement
    interpreter-core(Objects, Python, Grammar, and Parser dirs)
    3.15bugs and security fixes
    on Feb 26, 2026
  2. markshannon commented on Feb 26, 2026

    @markshannon
    MemberAuthor

    Minor correction:
    Some of the above PR suggest replacing PyTuple_Pack.
    In terms of clarity or safety there is no real improvement in using PyTuple_FromPair, but there is a significant performance advantage; PyTuple_Pack is quite cumbersome

  3. sergey-miryanov commented on Feb 26, 2026

    @sergey-miryanov
    Contributor

    If we decide to add this, I would like to work on it, if no one minds.

  4. markshannon commented on Feb 26, 2026

    @markshannon
    MemberAuthor

    If we decide to add this, I would like to work on it, if no one minds.

    Please, go ahead.

    I think the only real debate is which, if any, of these functions should go in the public API.
    If you make the functions private initially, then there should be no objection

    If you do want to add FromSingle and FromTriplet, could you do them in separate PRs later to avoid making the initial PR too big or contentious.

  5. vstinner commented on Mar 4, 2026

    @vstinner
    Member

    I think the only real debate is which, if any, of these functions should go in the public API.

    The C API Working Group voted against adding a public C API to create 1-tuple or 2-tuple. So I suggest adding an internal C API instead.

  6. sergey-miryanov commented on Mar 5, 2026

    @sergey-miryanov
    Contributor

    Results from microbenchmarks, run under Ubuntu 24.04, CPU i5-11600K @ 3.90GHz.

    When storing List to the tuple - List is gc-trackable, so tuple will be tracked too (microbenchmark):

    bench_tuple_new_pair: Mean +- std dev: 39.7 ns +- 2.6 ns
    bench_tuple_pack_pair: Mean +- std dev: 40.8 ns +- 1.5 ns
    bench_tuple_from_array_pair: Mean +- std dev: 36.4 ns +- 2.6 ns
    bench_tuple_from_array_pair_steal: Mean +- std dev: 35.8 ns +- 2.0 ns
    bench_tuple_from_pair: Mean +- std dev: 34.7 ns +- 2.1 ns
    bench_tuple_from_pair_steal: Mean +- std dev: 35.7 ns +- 1.9 ns
    

    When storing Long to the tuple - Long is not gc-trackable, so tuple will not be tracked (microbenchmark):

    bench_tuple_new_pair: Mean +- std dev: 22.9 ns +- 0.2 ns
    bench_tuple_pack_pair: Mean +- std dev: 20.9 ns +- 0.3 ns
    bench_tuple_from_array_pair: Mean +- std dev: 18.8 ns +- 0.1 ns
    bench_tuple_from_array_pair_steal: Mean +- std dev: 19.1 ns +- 0.2 ns
    bench_tuple_from_pair: Mean +- std dev: 18.4 ns +- 0.1 ns
    bench_tuple_from_pair_steal: Mean +- std dev: 16.1 ns +- 0.1 ns
    
    Script for run microbenchmarks
    import _testinternalcapi
    import pyperf
    
    
    def add_cmdline_args(cmd, args):
        cmd.append(args.what)
    
    
    def main():
    
        runner = pyperf.Runner(add_cmdline_args=add_cmdline_args)
        runner.argparser.add_argument('what', choices=['all', ])
        args = runner.parse_args()
    
        if args.what.lower() in ('all', ):
            runner.bench_time_func('bench_tuple_new_pair', _testinternalcapi.bench_tuple_new_pair)
            runner.bench_time_func('bench_tuple_pack_pair', _testinternalcapi.bench_tuple_pack_pair)
            runner.bench_time_func('bench_tuple_from_array_pair', _testinternalcapi.bench_tuple_from_array_pair)
            runner.bench_time_func('bench_tuple_from_array_pair_steal', _testinternalcapi.bench_tuple_from_array_pair_steal)
            runner.bench_time_func('bench_tuple_from_pair', _testinternalcapi.bench_tuple_from_pair)
            runner.bench_time_func('bench_tuple_from_pair_steal', _testinternalcapi.bench_tuple_from_pair_steal)
    
    
    
    if __name__ == '__main__':
        main()
    
  7. sergey-miryanov commented on Mar 5, 2026

    @sergey-miryanov
    Contributor

    I have a branch where I replaced old construction of 2-tuples with new API. It touches a lot of modules. Should I create a separate PR for each module?

  8. added a commit that references this issue on Mar 10, 2026
  9. vstinner commented on Mar 10, 2026

    @vstinner
    Member

    I merged #145325 which implements _PyTuple_FromPair() and _PyTuple_FromPairSteal().

    I have a branch where I replaced old construction of 2-tuples with new API. It touches a lot of modules. Should I create a separate PR for each module?

    I suggest creating a first PR which modify a maximum of 10 files. If this change is merged, move on to the next batch.

  10. sergey-miryanov commented on Mar 10, 2026

    @sergey-miryanov
    Contributor

    I suggest creating a first PR which modify a maximum of 10 files. If this change is merged, move on to the next batch.

    Thanks!

  11. added a commit that references this issue on Mar 11, 2026
  12. 5 remaining items

  13. added a commit that references this issue on Mar 28, 2026
  14. vstinner commented on Mar 30, 2026

    @vstinner
    Member

    @sergey-miryanov: You can CC-me if you write more changes to use these new functions.

  15. sergey-miryanov commented on Mar 30, 2026

    @sergey-miryanov
    Contributor

    @vstinner Ok, thanks! Will prepare PRs this week.

  16. added 2 commits that reference this issue on Apr 2, 2026
  17. added a commit that references this issue on Apr 16, 2026
  18. added 6 commits that reference this issue on Apr 25, 2026
  19. StanFromIreland commented on May 8, 2026

    @StanFromIreland
    Member

    Triage: Is work still on-going or can this be closed?

  20. sergey-miryanov commented on May 8, 2026

    @sergey-miryanov
    Contributor

    I plan to review the codebase for additional use cases for _PyTuple_FromPair, and evaluate whether adding _PyTuple_FromSingle and _PyTuple_FromTriplet variants makes sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions