Skip to content

Optimize collection literals with leading null unpack idiom in bytecode, allows bare BUILD_SET 0 #150737

Description

@gesslerpd

Feature or enhancement

Proposal:

Performance was discussed at various times within PEP 802 topic, submitting this proposal as the PEP is unlikely to be accepted. This is intended as an advanced escape-hatch not overlapping with the main reasons for PEP 802.

Goal:

  • Remove performance penalty of ast.unparse(ast.Set(elts=[])) ({*()} can be used in advanced cases and will not change recommended way to write empty set set())
  • Remove performance penalty of incremental construction between 0-to-1/1-to-0 elements for set and tuple literals without special case considerations (e.g. x = *(), pre-seeded tuple literal without trailing comma pitfall)
  • Keep implementation simple/self-contained in bytecode generation, avoiding any AST changes to language
  • Near-zero performance impact to bytecode generation (improves subsequent bytecode optimizations of target case)

Implementation examples:

Micro-benchmark

Run on windows x64bit release build: empty_unpack_benchmark.py

Compile time

Targeted cases:

Benchmark Main Flowgraph only Codegen leading only
[*()] 1.450 ms / 1.000x 1.393 ms / 0.961x 1.378 ms / 0.950x
{*()} 1.530 ms / 1.000x 1.488 ms / 0.973x 1.467 ms / 0.959x
(*(),) 1.916 ms / 1.000x 1.842 ms / 0.962x 1.832 ms / 0.956x
x = *(), 1.139 ms / 1.000x 1.088 ms / 0.955x 1.055 ms / 0.927x
xychart-beta
	title "Compile targeted mean ratio vs main"
	x-axis [Main, Flowgraph, Leading]
	y-axis "Ratio" 0 --> 1.01
	bar [1.000, 0.963, 0.948]
Loading

Control near-miss case comparisons moved less than +/-1% and are attributed to noise.

Runtime

The set() versus {*()} comparison is especially useful because it changes qualitatively across the two branches (~6.3% slower to ~29.5% faster):

Branch set() mean (ns) {*()} mean (ns) {*()} vs set()
Main 54.14 57.53 1.063x
Flowgraph only 53.49 38.01 0.711x
Codegen leading only 55.44 39.00 0.704x

Before this change, {*()} was paying for a redundant empty update and ended up slower than the constructor call. After the change, {*()} compiles down to a direct BUILD_SET 0; RETURN_VALUE path, while set() still has to load the global and perform a zero-argument call.

Has this already been discussed elsewhere?

I have already discussed this feature proposal on Discourse

Links to previous discussion of this feature:

https://discuss.python.org/t/pep-802-display-syntax-for-the-empty-set/101676/237
https://discuss.python.org/t/pep-802-display-syntax-for-the-empty-set/101676/216
https://discuss.python.org/t/pep-802-display-syntax-for-the-empty-set/101676/3

Linked PRs

Activity

  1. changed the title [-]Optimize leading null unpack idiom in bytecode, allows bare `BUILD_SET 0`[/-] [+]Optimize leading literal null unpack idiom in bytecode, allows bare `BUILD_SET 0`[/+] on Jun 18, 2026
  2. changed the title [-]Optimize leading literal null unpack idiom in bytecode, allows bare `BUILD_SET 0`[/-] [+]Optimize collection literals with leading null unpack idiom in bytecode, allows bare `BUILD_SET 0`[/+] on Sep 17, 2026
  3. added a commit that references this issue on Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions