PEP 802: Display Syntax for the Empty Set

I do that for personal projects, and at work, I get weird looks but no one has yet pushed back.

I suspect that if I tried to do that for other projects, I’d get more questions, and would have to follow their guidelines.

But, I can maintain my own little pocket of resistance.


I suppose the point I’m really trying to make it: I don’t think anything needs to be done.

If the compiler already has magic that turns foo = set() into a magical op code rather than a function call, cool. It would be neat to see that expanded to other containers for consistency. Also, what happens with callbacks like in defaultdict(). (I’ve not yet read the whole thread to know if this has already been mentioned.)

I would actually go the other way. Propose getting rid of [], {}, and () and only support the function calls. After all, a good compromise makes everyone unhappy. (And yes, this is a bit of a joke.)

1 Like

The importing of the expressiveness of Set Theory is good and all, but also causes people forgetting what they really are and importing definitions from math that do not apply to computer science:

Just like the constant flux of people wanting floats to be real or rational numbers, because their and their operations’ notation look so similar to that of real and rational numbers.


In that sense, I find the discomfort/surprise of {} producing dict and not set, a good remainder that the former are more fundamental in computer science, while “Sets are a fundamental mathematical structure” (PEP 218).

3 Likes

As some people have explained in this thread, due to the dynamic nature of Python it’s hard to make that optimization but it’s trivial for the literals. In any case, removing the literals sound like a bad idea to me. It would destroy backwards compatibility and I quite like literals.

2 Likes

I’m -1ish[1].

My views on this are the same as those on (e.g.) the annual proposals to rename or alias decades-old APIs to make them PEP-8 compliant: the consistency isn’t worth the churn such an addition might cause in the long run.


  1. both regarding the PEP and the alternatives raised throughout the thread ↩︎

5 Likes

Even as an ex-mathematician, I find the visual parallel to the empty set questionable. I’d much rather have a consistent set of literals, at least to the degree that’s possible with sets and dicts sharing the same delimiter. I also don’t think that the way of spelling frozenset is a relevant concern here.

I think this is the most compelling option so far, and IMO fits nicely with the dict-unpacking discussion. {**} could simply be read as unpacking nothing, and getting an empty dict.

And rather than having to explain away inconsistencies between {/}, {,}, {:} etc. with lists and tuples, the distinction becomes clear based on the internal structure, which people have already learned is the differentiating quality between dicts and sets. So this would give

empty_1d = []  # or () or set()
empty_2d = {}
 [*] ==  [*empty_1d] == [] == list()
 (*) ==  (*empty_1d) == () == tuple()
 {*} ==  {*empty_1d}       == set()
{**} == {**empty_2d} == {} == dict()

This also creates a nice parallel with the {*()} variant that’s been brought up several times – just leave out the empty argument to the asterisk.

[*empty_1d], {*empty_1d} and {**empty_2d} work like that today already; so do the “dimensionally mismatched” [*empty_2d] and {*empty_2d} (which isn’t ideal but also not a big problem IMO) – the latter creates a set, matching the “single asterisk” interpretation! :slight_smile:

{**empty_1d} does not work (and it shouldn’t, arguably), and neither does (*empty_1d), though the latter could presumably be allowed for consistency.

23 Likes

@h-vetinari

Back in June, I created a mini-draft PEP along these lines but shelved it, to my surprise this is now back in discussion would like to post excerpt from the beginning of it. Also provides a solution for placeholder empty tuple that can be easily extended without knowing trailing comma rules. This type of null unpacking doesn’t need to be represented in the AST.

This is presented as a generalized expansion of the concept, but can imagine reduced scope subset variants of the same idea:

  • Limited to first literal element position in grammar to avoid confusion and potential typos of it being used later (would be no-op regardless). e.g. (*,1) is valid, (1, *,) is invalid
  • Omitting {**} part, at expense of symmetry, because less useful than the * forms (especially sets and tuples)
  • Limiting it to only the first element position in sets and tuples, at expense of more symmetry
  • Scope reduced to the level of current proposal, special casing {*} and not allowing {*,}, I believe this is the best way to disambiguate between dict/set semantics because of the existing {*iterable} and {**mapping} language behaviors, {,} is ambiguous between dict/set.

PEP: Null Literal Unpacking

Abstract

This PEP introduces a “null unpacking” syntax for list, tuple, set, and dictionary literals, allowing the use of bare * and ** operators with no expression to produce an empty literal of that type. These forms are:

Literal Type Syntax Equivalent to
List [*] []
Tuple (*,) ()
Set {*} set() but faster since compiles to empty set opcodes
Dict {**} {}

The feature adds symmetry between unpacking and literal syntax and provides an explicit, visually consistent way to represent empty literals in unpacking contexts. It also introduces a placeholder-friendly form for tuples that makes adding future elements simpler.


Motivation

Python already supports unpacking within literal displays:

[*a, *b]       # list unpacking
(*a, *b)       # tuple unpacking
{*a, *b}       # set unpacking
{**x, **y}     # dict unpacking

However, an empty unpacking ([*], {*}, (*,), {**}) is currently a SyntaxError. Null-unpack literals provide a consistent way to represent empty collections using familiar unpacking syntax, improving readability and consistency.

Placeholder-friendly tuples

Creating a tuple placeholder without elements currently requires working around trailing-comma rules. Null-unpack tuples allow:

coords = (*,)       # empty tuple placeholder
coords = (*, 1)     # easily extend later
coords = (*, 1, 2)  # continue extending

This is consistent with Python’s tuple syntax and simplifies incremental construction.


Rationale

Consistency

Null-unpack literals complete the symmetry of PEP 448:

Current valid Current invalid Proposed valid
[*a] [*] [*]
(*a,) (*,) (*,)
{*a} {*} {*}
{**a} {**} {**}

Readability

It is immediately clear that a literal is empty or a placeholder.

Placeholder semantics

Supports positions in tuples for future expansion without requiring dummy values.

Performance

Null-unpack literals compile directly to the corresponding empty literal opcodes:

  • [ * ]BUILD_LIST 0

  • { * }BUILD_SET 0

  • (* ,)BUILD_TUPLE 0

  • { ** }BUILD_MAP 0

No runtime evaluation is needed.

AST Impact

  • Null-unpack literals do not create new AST nodes.

  • [ * ,]List(elts=[], ctx=Load())

  • { *, }Set(elts=[], ctx=Load())

  • (* ,)Tuple(elts=[], ctx=Load())

  • { **, }Dict(keys=[], values=[])

  • Starred nodes are omitted entirely.

This ensures AST reflects semantics, not source formatting. CST-based tools may still preserve the exact syntax.

28 Likes

The syntax proposed by @gesslerpd is quite elegant, and I support this over the other options discussed. +1.

3 Likes

I proposed ‘{*}’ as a possibility (edit: but did not connect it to unpacking). I think generalizing it makes it more appealing.

8 Likes

Yeah I think it’s easier to get to the {*} conclusion if you have the generalization in mind, the only good reason I could come up with, other than symmetry, when I was thinking about it was (*,) isn’t as prone to the () → try to add element later when aren’t paying as much attention → (1) no longer tuple gotcha.

One interesting part of the generalization is that (*) is invalid, because (*iterable) is invalid but this actually makes the tuple use-case easier to avoid the gotcha.

Even if {*} is special cased to start, within the scope of this current proposal, it still leaves the generalization open for discussion to be added later w/o any issues. Admittedly the main purpose is to have a empty set literal :grinning_face:

I am mildly against adding {/}.

  • The problem itself seems small. set()is clear enough, in my opinion.
  • The learning tradeoff is obscure. Beginners will learn that {} is a dict, but {/} is a special case. And special cases aren’t special enough to break the rules.
  • The addition would lead us to some slippery arguments like: why not [/], (/), and whatever.
  • What about the ecosystem? Docs, snippets, tutorials, linters, formatters… Would this addition justify the overhead?

Something we can do is to focus on better messages. Suggestion:

s = {}
s.add(1)
Traceback (most recent call last):
File “”, line 1, in
s.add(1)
^^^^^
AttributeError: ‘dict’ object has no attribute ‘add’
{} is an empty dict. Use set() for an empty set.

5 Likes

This is very good. I think any solution that can provide an empty set and give us a way to spell empty tuple while using a , is a stronger PEP.

One point of clarity: you noted that (*a,) is already valid, so is {*a,}. I presume then that along with (*,), {*,} {**,} [*,] will also all become valid, even if perhaps not recommended.

Yes that’s correct, the expanded generalization is as follows:

{*} == {*,} == set()

[*] == [*,] == []

{**} == {**,} == {}

(*,) == () and (*) is invalid because (*iterable) is invalid, but helps avoid the pitfall upon modifying tuple from 0 to 1 element or vice-versa.

8 Likes

Out of naive interest, what is the logic/advantage of (*iterable) being invalid? That would have been surprising to me had I wanted to create a tuple by unpacking, especially given that bare () does create a tuple and isn’t considered ambiguous.

1 Like

My guess is, and there might be other reasons, is this

A parenthesized expression list yields whatever that expression list yields: if the list contains at least one comma, it yields a tuple; otherwise, it yields the single expression that makes up the expression list.

For example, (*[1, 2, 3]) should result in the unpacked 1, 2, 3, out not in a list or anywhere.


Now, assume that you were to agree that (*[1, 2, 3]) results in (1, 2, 3). The case (*[1]) is still not clear. Should it be 1 or (1,)?


I think this is to do with generator expressions.

To avoid confusion.

It is already confusing that (i for ... in ...) is generator, while () is empty tuple.

This is because (obj) == obj, so to disambiguate from the normal parentheses operation a singleton tuple literal is required to add a trailing comma when enclosed in parentheses, e.g. (obj,), and for consistency this requirement is extended to unpacking a single iterable in a tuple literal, where it has to be written as (*iterable,).

3 Likes

A comparison of insertion/deletion edit distance

Distance of 1 keystroke:

{,}   # Initial null set
{1,}  # Insert 1

Distance of 2 keystrokes:

{/}   # Initial null set
{1/}  # Insert 1
{1}   # Delete /

Clearly, {,} is far more natural since it’s literally a subset (pun intended) of ASCII characters.

Bonus: black can auto-format {1,}{1}.


A fun functorial property of {,}

Let:

{,} ⊂ {1,} ⊂ {1,2} ⊂ {1,2,3}

This ordering is preserved under the transformation lambda x: set(str(x)):

set("{,}") ⊂ set("{1,}") ⊂ set("{1,2}") ⊂ set("{1,2,3}")

Fun.

6 Likes

I see () to represent empty tuple as a special case in the language. Tuple literals can be created without parenthesis in some contexts e.g. a = 1, so this syntax provides a way to write empty tuple without parenthesis in those same contexts e.g. a = *, where a == (*,) == ().

With or without the parenthesis, this allows the empty tuple to be extended easily because it forces the “comma” which is more representative of a tuple in all but the current empty special-case.

I’m -1 on this proposal for several reasons.

My primary concern is readability. While I understand the connection to ∅, I don’t find {/} visually intuitive when scanning through code. The slash character doesn’t have any established meaning in Python for “emptiness” or “absence,” so this feels like an arbitrary symbol choice that requires memorization rather than something that follows from the language’s existing patterns.

Additionally, set() is already clear and unambiguous. It follows the same pattern as list(), dict(), and tuple(), creating a consistent mental model: “to create an empty container, call its constructor.” This consistency has pedagogical value that I think we’re underestimating.

I’m also concerned about the long-term ecosystem implications. We’ll have two ways to spell the same thing indefinitely, which means:

  • Both forms will appear in codebases, requiring developers to recognize both

  • Documentation and tutorials will need to explain both (or make a choice and potentially confuse readers who encounter the other form)

  • Code reviews will see bikeshedding about which form to use

  • Linters may add rules about preferring one over the other, creating more configuration overhead

The problems this solves (avoiding name lookups, handling shadowed names, providing a literal syntax) are either already solvable with {*()} or minor enough that I don’t think they justify the additional complexity.

If we’re going to add new syntax, I’d rather see something that addresses a more significant pain point or enables new capabilities, rather than providing a third way to create an empty set.

5 Likes

Has {.} been considered? It is visually less noisy than alternatives, and easy to type.