Skip to content

Conversation

@nineteendo
Copy link
Owner

@nineteendo nineteendo commented Apr 12, 2025

Frozen set literals and comprehensions

We should embrace the frozen set, literally: {{...}}.

Motivation

Currently you need too many characters to construct a frozen set:

foo = frozenset()
bar = frozenset({1, 2, 3})
baz = frozenset({a, b, c})
qux = frozenset({c * 2 for c in "abc"})

Additionally, this is very inefficient:

>>> from dis import dis
>>> dis('foo = frozenset()')
  0           RESUME                   0

  1           LOAD_NAME                0 (frozenset)
              PUSH_NULL
              CALL                     0
              STORE_NAME               1 (foo)
              RETURN_CONST             0 (None)
>>> dis('bar = frozenset({1, 2, 3})')
  0           RESUME                   0

  1           LOAD_NAME                0 (frozenset)
              PUSH_NULL
              BUILD_SET                0
              LOAD_CONST               0 (frozenset({1, 2, 3}))
              SET_UPDATE               1
              CALL                     1
              STORE_NAME               1 (bar)
              RETURN_CONST             1 (None)
>>> dis('baz = frozenset({a, b, c})')
  0           RESUME                   0

  1           LOAD_NAME                0 (frozenset)
              PUSH_NULL
              LOAD_NAME                1 (a)
              LOAD_NAME                2 (b)
              LOAD_NAME                3 (c)
              BUILD_SET                3
              CALL                     1
              STORE_NAME               4 (baz)
              RETURN_CONST             0 (None)
>>> dis('qux = frozenset({c * 2 for c in "abc"})')
   0           RESUME                   0

   1           LOAD_NAME                0 (frozenset)
               PUSH_NULL
               LOAD_CONST               0 ('abc')
               GET_ITER
               LOAD_FAST_AND_CLEAR      0 (c)
               SWAP                     2
       L1:     BUILD_SET                0
               SWAP                     2
       L2:     FOR_ITER                 7 (to L3)
               STORE_FAST_LOAD_FAST     0 (c, c)
               LOAD_CONST               1 (2)
               BINARY_OP                5 (*)
               SET_ADD                  2
               JUMP_BACKWARD            9 (to L2)
       L3:     END_FOR
               POP_TOP
       L4:     SWAP                     2
               STORE_FAST               0 (c)
               CALL                     1
               STORE_NAME               1 (qux)
               RETURN_CONST             2 (None)

  --   L5:     SWAP                     2
               POP_TOP

   1           SWAP                     2
               STORE_FAST               0 (c)
               RERAISE                  0
ExceptionTable:
  L1 to L4 -> L5 [4]

That's why it could be useful to have frozen set literals and comprehensions:

foo = {{/}}
bar = {{1, 2, 3}}
baz = {{a, b, c}}
qux = {{c * 2 for c in "abc"}}

Then this is all we need to do:

>>> from dis import dis
>>> dis('foo = {{/}}')
  0           RESUME                   0

  1           LOAD_CONST               0 ({{/}})
              STORE_NAME               0 (foo)
              RETURN_CONST             1 (None)
>>> dis('bar = {{1, 2, 3}}')
  0           RESUME                   0

  1           LOAD_CONST               0 ({{1, 2, 3}})
              STORE_NAME               0 (bar)
              RETURN_CONST             1 (None)
>>> dis('baz = {{a, b, c}}')
  0           RESUME                   0

  1           LOAD_NAME                0 (a)
              LOAD_NAME                1 (b)
              LOAD_NAME                2 (c)
              BUILD_FROZENSET          3
              STORE_NAME               3 (baz)
              RETURN_CONST             0 (None)
>>> dis('qux = {{c * 2 for c in "abc"}}')
   0           RESUME                   0

   1           LOAD_CONST               0 ('abc')
               GET_ITER
               LOAD_FAST_AND_CLEAR      0 (c)
               SWAP                     2
       L1:     BUILD_FROZENSET          0
               SWAP                     2
       L2:     FOR_ITER                 7 (to L3)
               STORE_FAST_LOAD_FAST     0 (c, c)
               LOAD_CONST               1 (2)
               BINARY_OP                5 (*)
               SET_ADD                  2
               JUMP_BACKWARD            9 (to L2)
       L3:     END_FOR
               POP_TOP
       L4:     SWAP                     2
               STORE_FAST               0 (c)
               STORE_NAME               0 (qux)
               RETURN_CONST             2 (None)

  --   L5:     SWAP                     2
               POP_TOP

   1           SWAP                     2
               STORE_FAST               0 (c)
               RERAISE                  0
ExceptionTable:
  L1 to L4 -> L5 [2]

Syntax

frozenset_display ::= "{{" (`starred_list` | "/" | `comprehension`) "}}"

Note

Technically the syntax is this:

frozenset_display ::= "{" "{" (`starred_list` | "/" | `comprehension`) "}" "}"

But that's an implementation detail that shouldn't be documented.

Example

assert {{/}}             == frozenset()
assert {{1, 2, 3}}       == frozenset({1, 2, 3})
assert {{{1, 2, 3}}}     == {frozenset({1, 2, 3})}
assert {{{{1, 2, 3}}}}   == frozenset({frozenset({1, 2, 3})})
assert {{{{{1, 2, 3}}}}} == {frozenset({frozenset({1, 2, 3})})}
...
assert {{c * 2 for c in "abc"}} == frozenset({c * 2 for c in "abc"})

Backwards compatibility

These statements would behave differently with this proposal:

foo = {{1, 2, 3}}        # TypeError: unhashable type: 'set'
bar = {{{1, 2, 3}}}      # TypeError: unhashable type: 'set'
baz = {{{{1, 2, 3}}}}    # TypeError: unhashable type: 'set'
qux = {{{{{1, 2, 3}}}}}  # TypeError: unhashable type: 'set'
...
foo = {{c * 2 for c in "abc"}}  # TypeError: unhashable type: 'set'

But I don't think they can be used for anything useful, -'' is a shorter way to raise a type error.

Pros

  • Symmetrical and only uses brackets
  • Doesn't raise syntax error in previous versions
  • Doesn't reserve any new syntax
  • Double braces can look intuitively as a hardened set
  • Some other languages already give special meaning to {{...}}
  • Some text editors already support embracing selected text

Cons

  • Potentially breaks backwards compatibility
  • Double punctuation isn't Pythonic, there's a precedent with triple quotes: '''''+'''''
  • Suffers from brace overflow

GitHub usage

Other suggestions for frozenset literals

expand

{1, 2, 3}.freeze()

Example:

assert {1, 2, 3}.freeze() == frozenset({1, 2, 3})

Pros:

  • Intuitive
  • Doesn't raise syntax error in previous versions

Cons:

  • Hard to maintain
  • Unclear that it wouldn't be copied at runtime
  • Doesn't improve representation

{1, 2, 3}

Example:

assert {1, 2, 3} == frozenset({1, 2, 3})
assert {1, 2, 3} != set({1, 2, 3})

Pros:

  • Symmetrical and only uses brackets

Cons:

  • Not backwards compatible
  • Inconsistent way to get mutable collections
  • No unambiguous notation for set literals

|1, 2, 3|

Example:

assert |1, 2, 3| == frozenset({1, 2, 3})

Cons:

  • Undirectional
  • Can't keep track of nesting

<1, 2, 3>

Example:

assert <1, 2, 3> == frozenset({1, 2, 3})

Pros:

  • Symmetrical and only uses brackets

Cons:

  • Can't keep track of nesting
  • Hard to read
  • Already used as operator

f{1, 2, 3}

Example:

assert f{1, 2, 3} == frozenset({1, 2, 3})

Pros:

  • Prefix can be easily added and removed

Cons:

  • Rules out possibility of foo{...}
  • Not obvious
  • Looks like slicing operator or function call
  • Endless arguing over s{} for empty set

{{1, 2, 3}}

Example:

assert {{1, 2, 3}}       == frozenset({1, 2, 3})
assert {{{1, 2, 3}}}     == {frozenset({1, 2, 3})}
assert {{{{1, 2, 3}}}}   == frozenset({frozenset({1, 2, 3})})
assert {{{{{1, 2, 3}}}}} == {frozenset({frozenset({1, 2, 3})})}
...

Pros:

  • Symmetrical and only uses brackets
  • Doesn't raise syntax error in previous versions
  • Doesn't reserve any new syntax
  • Double braces can look intuitively as a hardened set
  • Some other languages already give special meaning to {{...}}
  • Some text editors already support embracing selected text

Cons:

  • Potentially breaks backwards compatibility
  • Hard to read and parse
  • Double punctuation isn't Pythonic, there's a precedent with triple quotes: '''''+'''''
  • Suffers from brace overflow
  • Requires a lot of changes to tokenisation and string representations

|{1, 2, 3}|

Example:

assert |{1, 2, 3}| == frozenset({1, 2, 3})

Cons:

  • PEP 351 was rejected
  • Undirectional

Links

  1. https://peps.python.org/pep-0351
  2. https://mail.python.org/pipermail/python-3000/2008-January/thread.html#11798
  3. https://mail.python.org/archives/list/[email protected]/thread/MVIIUMQZYTTSGZSYJFGKPHTOF5Y4RI6I
  4. https://mail.python.org/archives/list/[email protected]/thread/AMWKPS54ZK6X2FI7NICDM6DG7LERIJFV
  5. https://mail.python.org/archives/list/[email protected]/thread/SOGSM2KVVNYLD2U2EUJHOPZW7BUNOOF2
  6. https://mail.python.org/archives/list/[email protected]/thread/M6TMP3HRNA7HHF2S6R4VCZCTRDZ4W6WX
  7. https://mail.python.org/archives/list/[email protected]/thread/GRMNMWUQXG67PXXNZ4W7W27AQTCB6UQQ
  8. https://discuss.python.org/t/make-using-immutable-datatypes-more-pleasant-by-adding-a-little-syntactic-sugar/23588
  9. https://discuss.python.org/t/alternative-call-syntax/53126
  10. https://discuss.python.org/t/frozen-set-literals/53489

📚 Documentation preview 📚: https://nineteendo-cpython--19.org.readthedocs.build/

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants