Skip to content

Embrace the frozen set #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 104 commits into
base: main
Choose a base branch
from
Open

Embrace the frozen set #19

wants to merge 104 commits into from

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.

2 participants