Skip to content

sets with >30 elements not constant-folded in in/not in and for loops #148817

Description

@itamaro

Bug report

Bug description:

Since gh-126835 (moving constant folding from AST to CFG), set and list literals with more than 30 constant elements are no longer constant-folded in in/not in membership tests and for loop iteration. This causes the collection to be rebuilt from scratch on every evaluation instead of being stored as a compile-time constant.

For example:

import dis

# 30 elements: folded to frozenset constant
dis.dis("x in {%s}" % ", ".join(str(i) for i in range(1, 31)))
#   LOAD_NAME   x
#   LOAD_CONST  frozenset({1, 2, ..., 30})
#   CONTAINS_OP

# 31 elements: NOT folded, set rebuilt every time (BAD)
dis.dis("x in {%s}" % ", ".join(str(i) for i in range(1, 32)))
#   LOAD_NAME   x
#   BUILD_SET   0
#   LOAD_SMALL_INT 1
#   SET_ADD     1
#   ... (31 x LOAD + SET_ADD)
#   CONTAINS_OP

Using a set-membership check with a set literal with 50 elements as a microbenchmark, this manifests as a ~20x slowdown from 3.13 to 3.14:

import timeit, sys
elts = ", ".join(str(i) for i in range(50))
t = timeit.timeit(f"25 in {{{elts}}}", number=1_000_000)
print(f"25 in {{0..49}}: {t:.3f}s")

Python 3.13.13: 25 in {0..49}: 0.015s
Python 3.14.4: 25 in {0..49}: 0.291s

It also affects code object size:

for n in [30, 31, 50, 100]:
    elts = ", ".join(str(i) for i in range(1, n + 1))
    code = compile(f"x in {{{elts}}}", "<test>", "eval")
    print(f"x in {{1..{n}}}: {len(code.co_code):>5} bytes, {len(code.co_consts)} const(s)")

Python 3.13.13:

x in {1..30}:    12 bytes, 1 const(s)
x in {1..31}:    12 bytes, 1 const(s)
x in {1..50}:    12 bytes, 1 const(s)
x in {1..100}:    12 bytes, 1 const(s)

Python 3.14.4:

x in {1..30}:    12 bytes, 2 const(s)
x in {1..31}:   136 bytes, 1 const(s)
x in {1..50}:   212 bytes, 1 const(s)
x in {1..100}:   412 bytes, 1 const(s)

CPython versions tested on:

3.15, CPython main branch, 3.14

Operating systems tested on:

macOS, Linux

Linked PRs

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    performancePerformance or resource usage
    interpreter-core(Objects, Python, Grammar, and Parser dirs)
    on Apr 21, 2026
  2. itamaro commented on Apr 21, 2026

    @itamaro
    ContributorAuthor
  3. picnixz commented on Apr 21, 2026

    @picnixz
    Member

    How likely are there sets of 30+ elements that need to be stored as real constants? I can see the set of ascii letters+digits being such examples thereof or essentially unicode ranges. What would be a good cutoff here? 50? 100? 70? 52? 62? (I would say 64 just to have lower/upper case + digits)

  4. added
    3.14bugs and security fixes
    3.15bugs and security fixes
    on Apr 21, 2026
  5. self-assigned this
    on Apr 21, 2026
  6. itamaro commented on Apr 21, 2026

    @itamaro
    ContributorAuthor

    How likely are there sets of 30+ elements that need to be stored as real constants?

    I came across this because it came up in real world code :)

    I ran some analyses and found at least 95 files with this pattern (with set/list literals with more than 30 elements). most were in the 30-50 elements range.

    here's one example I found from numpy (with 51 elements): https://github.com/numpy/numpy/blob/388bd75829842d8230147f57eba5f86bb3ad6c3c/numpy/f2py/crackfortran.py#L253-L261

    beyond that, I had a small number of hits with collections of sizes 51 to 120.

  7. picnixz commented on Apr 21, 2026

    @picnixz
    Member

    I think having 64 as a cutoff would cover most use cases. It covers alnum ranges. We can make it 256 as well just to cover the entire ASCII range (conceptually). What is the cost of having a much larger length? like, it would only slow down the bytecode generation (so negligible cost), and bump the process' memory for those constant-folded objects right?

  8. iritkatriel commented on Apr 21, 2026

    @iritkatriel
    Member

    Do you know which commit introduced this?

  9. itamaro commented on Apr 21, 2026

    @itamaro
    ContributorAuthor

    Do you know which commit introduced this?

    a combination of 140e69c and 3618240 I believe

  10. itamaro commented on Apr 21, 2026

    @itamaro
    ContributorAuthor

    @picnixz re cutoff -- I am not knowledgeable enough to say, as I don't fully understand the tradeoffs.

    based on asking Claude Code to analyze this more closely, it claims that the original behavior (AST optimizer) had no limit when constant-folding, and even now, fold_constant_intrinsic_list_to_tuple would constant-fold tuples with no size limit. the "cost" of unlimited constant-folding would be compile time, and presumably shifting such cost from runtime to compile time is almost always beneficial (and would also net a small pyc size win since a marshalled frozenset of size N would be smaller than the equivalent sequence of bytecode to dynamically construct it at runtime).

    Claude Code went further and implement the optimization (fold_const_set_or_list_add, based on fold_constant_intrinsic_list_to_tuple), but given my lack of expertise in this area, I can't "own" that optimization to the level I would be comfortable making a PR based on it (I can push it to a branch in my fork if someone else might find it useful though).

  11. kumaraditya303 commented on Apr 22, 2026

    @kumaraditya303
    Contributor

    I don't think we need any cutoff here, runtime performance matters much more than bytecode generation.

  12. Eclips4 commented on Apr 22, 2026

    @Eclips4
    Member

    Yeah, that's certainly because of moving things from ast_opt.c to flowgraph.c. In flowgraph.c we basically use a compile-time constant _PY_STACK_USE_GUIDELINE for almost all of those optimizations, and by default it's equal to 30, so we don't optimize sequences longer than that. And yes, this was an intentional change, sorry if we forgot to mention it explicitly.

    @itamaro I'd be happy to take a look at your PR =)

  13. itamaro commented on Apr 22, 2026

    @itamaro
    ContributorAuthor

    @itamaro I'd be happy to take a look at your PR =)

    @Eclips4 see itamaro@6c9b972 with the disclaimer that it's fully claude-authored. I ran it, but I didn't look at it!

  14. Eclips4 commented on Apr 22, 2026

    @Eclips4
    Member

    Thanks Itamar!
    I'll do a PR on the weekend.

  15. Eclips4 commented on Apr 26, 2026

    @Eclips4
    Member

    FYI folks I'm going to merge #146456 to make things faster when we have long sequences with constants. Otherwise we could get bad compilation times in those cases.

  16. Eclips4 commented on Apr 26, 2026

    @Eclips4
    Member

    PR #149016 is up. @iritkatriel @itamaro, I'd really appreciate a review and verification from you.

  17. added 2 commits that reference this issue on Aug 16, 2026
  18. added a commit that references this issue on Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

3.14bugs and security fixes3.15bugs and security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)performancePerformance or resource usagetype-bugAn unexpected behavior, bug, or error

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions