PEP 802: Display Syntax for the Empty Set

As someone from the PyDis community who frequently helps beginners, I often see confusion around the fact that Python doesn’t have a literal for an empty set.

A very common beginner mistake is to write {} expecting an empty set, only to later discover, that it’s actually an empty dictionary.

a set literal would eliminate a surprisingly common and frustrating source of confusion imo.

4 Likes

Will PEP 802 change that in any way? {} is still the most natural way to write an empty set, and it will still be a dictionary.

TBH I think the problem here is that Python uses single-character delimiters. There just aren’t enough of them. We have the same problem with single-element tuples, where the trailing comma isn’t optional. If, instead, all collection displays were written with two-character delimiters, we could have ([ ]) ({ }) (< >) without ambiguity. Obviously it’s way WAY too late to change that now, though.

6 Likes

{/} as a syntax better relates to {a} than set() does, potentially making it easier for beginners to remember. Whether this makes a significant difference, IDK.

1 Like
uv run python
Python 3.13.5 (main, Jul 23 2025, 00:18:28) [Clang 20.1.4 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> print(type({}))
<class 'dict'>

{} is a dictionary, not an empty set, having a clear way to instantiate just an empty set just like there is for dicts and list will help a ton imo

Yes, but it also relates to {a:b} so it’s likely to be just as much of a problem.

To any educators out there, I’d love a poll among your students. Match the expressions to what you expect their type or meaning should be:

{}
{/}
()
(,)
{,}
<>

Available responses: dict, set, tuple, vector, Not equal, and “that’s invalid, it should be a SyntaxError”. Notably, I would be interested to see what novice programmers would expect of this proposed sequence, but also if there’s any inherent expectation that would lead to a natural syntax for vectors.

7 Likes

I’m sympathetic to the goals of this PEP, but I think that as long as {} means an empty dict, all attempts in this area are futile. Even the small cost in knowledge requirements (i.e., you now need to know this new syntax to confidently read Python code) isn’t worth the even smaller benefit. If in the mythical era of Python 4 we have the chance, we can redefine {} as empty set and {:} as empty dict, and that would be worth it. But if we can’t fix the wart fully, I don’t see half-measures like this as worthwhile.

17 Likes

HIGHLY unlikely. Even supposing that such a drastically backward-incompatible change could be done (and the very worst kind of incompatible, where the meaning of something changes and there’s basically no way to write code that works correctly on both), that would elevate sets to be superior to dictionaries. Dictionaries are everywhere in Python, and creating them - even empty ones - is an extremely common operation; sets, by comparison, are vanishingly rare.

6 Likes

If beginners write {} for an empty set without remembering that there is no empty set literal, they will likely continue doing so even if {/} becomes the official empty set literal. Remembering this is simply something to learn; it has no logical connection to other parts of the language and no precedent in other programming languages I’m aware of. It is a weird mix of ∅ and {}, which are equivalent. So {/} ≠ ∅ because it’s not empty; it has one element: the slash.

I would still continue writing set() to avoid adding a redundant comment like {/} # set()

Note that Python isn’t only used by programmers; consuming an API interface often requires little to no programming experience. With simple pattern recognition, which humans excel at, one can replace the necessary parts of the code, and voilà.

4 Likes

they typically don’t even learn whether there is or isn’t a set literal in the first place, they simply learn about list and dict literals, and when they finally start learning about sets, they intuitively try to use a set literal, much to their confusion.

1 Like

Indeed, and optimizing the well-known set() idiom in the JIT to avoid a lookup and the call entirely is not difficult, if we decide we want to (I just tried it locally, and safely turning the whole thing into just PySet_New(NULL) and an error check is only a 33-line change using our current optimizer).

Separately, from the motivation of the PEP:

For example, users must be careful not to use set as a local variable name, as doing so prevents constructing new sets. This can be frustrating as beginners may not know how to recover the set type if they have overriden the name. Techniques to do so (e.g. type({1})) are not immediately obvious, especially to those learning the language, who may not yet be familiar with the type function.

This motivation seems very weak to me. This same reasoning applies to almost all builtins: “users must be careful not to use len as a local variable name, as doing so prevents getting the length of objects”.

I’m not convinced that this is a problem at local scope, because those aren’t really used interactively (I have trouble imagining a scenario where you’ve shadowed set in the local scope, and then had to create a set without the ability to go back and fix your mistake… maybe debugging?). In the interactive REPL/notebook case, you’ve only shadowed it in the globals, so a simple whoops = set; del set should suffice. And I’m not sure most beginners encounter set before type, but that may just be my own experience.

So from my perspective, this proposal doesn’t justify itself much on performance, usability, or aesthetic grounds (I find it unintuitive and… not very pretty). It seems to mostly arise from a vague appeal to consistency, but the novel use of / throws that argument out the window in my view too.

I don’t have much to say about the beginner experience[1] or providing a “culture-free notation”[2], but count me -1.


  1. …except that this seems like just an additional special case to learn which doesn’t make the {} footgun any less appealing. ↩︎

  2. …except that I’m not quite sure what it means, since basic English skills seem required to make use of most builtins currently. ↩︎

22 Likes

One small benefit of the dedicated syntax is that, unlike set() or {*()}, it would be supported by ast.literal_eval(). This would make it easy to safely parse data representing all basic containers.

It’s already “supported” via a special case in pattern matching.

personally I don’t think this special case should be deprecated if this PEP is accepted since that’d be backwards incompatible for such a small change.

6 Likes

I’m almost entirely in the “potentially nice in a new language, not worth it at this late stage for Python” camp (given set() and the {*()} workaround for cases where shadowing is a problem), but I’d potentially be more interested if the proposal was to add both {:} and {/} (with the former being equivalent to {}, and the latter being equivalent to {*()} after constant folding), rather than only proposing to add {/} and keeping the now ambiguous {} as the only syntactic spelling of empty dictionaries.

That said, while I do find the status quo mildly irritating, I also know it only exists for historical reasons and trying to introduce new spellings now would inevitably make things more confusing rather than less (as even new users frequently end up maintaining old code and would need to learn both of the defined spellings eventually anyway).

25 Likes

+0.5 from me.

All the code bases I work in generally prefer dict(), list(), set() over {}, [], {*()}, so this is not actually likely to impact me.

But it would feel good knowing that {/} exists. And it would be a more pleasing repr.

A counter point to some of the arguments against: I am aware of both list() and [], and I don’t think this has ever caused me (or anyone else I know) any confusion. There are code conventions in place such that there generally is one preferred choice between list() and []. Thinking about it, these conventions are actually hard to define, but I know the correct choice when I see it. The non-existence of an empty set literal sometimes (in very rare circumstances) makes it impossible to apply those conventions. (Or another way to look at it is that they force special-casing for sets.)

1 Like

Comparing the use of existing ways to construct empty list/dict/tuple it seems that the shorthand has is more common in the stdlib (excluding tests).

  • Lists
    Shorthand ([]): 946
    Full (list()): 1

  • Tuples
    Shorthand (()): 158
    Full (tuple()): 3

  • Dicts
    Shorthand ({}): 158
    Full (dict()): 0

5 Likes

I can see the appeal of the proposal – but I am against it for these reasons:

  1. {/} is (IMO) harder to type than set(): on a standard QWERTY/US layout, it is
  • set() is s, e, t, Shift 9 0 Shift-Up (7 keys, and I can type s, e, t very quickly since they are the top letters in common English words)
  • {/} is: Shift, [, Shift-Up / Shift, ], Shift up. Also 7 keys, but involving less commonly typed lettters.
  1. {/} is harder to read: when scanning through code, set() is more evocative than {/} as the latter is too visually similar to {}. In a code editor, this is more so the case because set will likely be syntax highlighted.

These are minor reasons. The more substantive reasons are:

  1. Backward compatibility. Given that set() does the same thing, I would avoid using {/} since it introduces a spurious version dependency.

    (I sympathize with the thinking that it’s “unfair” for {/} to bear the burden of backward compatibility … but in the end, that’s the reality and my users don’t care about that.)

  2. With the rapid progress and rise in popularity of static type checkers, I’ve recently gotten into a habit of doing this:

x = set[int]()
for some_logic in (...):
    (... remove or add elements into x ...)

This plays well with type checkers: they can now infer that x should hold integers. In fact: adding/removing elements accounts for an overwhelming fraction of cases where I would reach for a literal empty set, and in these cases it is also overwhelmingly likely I would like to make use of type checking.

In this way, list[T](), dict[K,V](), etc. are actually better than [] and {}. I feel that introducing {/} would actually discourage the better practice of set[T](). (“Better” is just my opinion though!)

13 Likes

A little offtopic, but you don’t need to do this for Mypy.

my_set = set()

for i in range(10):
    my_set.add(i)
    
reveal_type(my_set)  # builtins.set[builtins.int]

To my eyes at least doing list[int]() over [] is going to be significantly less readable and less performant.

3 Likes

I see - I was using basedpyright, where in one circumstance (more complicated than the above) I needed the hint.

I think beauty’s in the eye of the beholder: I much prefer list[int]() over [] – as soon as I see list[int](), I can stop reading and know it’s a integer list. For [], I have to run a Python interpreter in my head to determine that. And probably the performance benefit of list[int]() minuscule over [], e.g. for projects where I’m already using Python…

I see where you’re coming from though - definitely no black/white rules here!

Today I learned you can call through a types.GenericAlias :slightly_smiling_face: . Looks like this can give more understandable code in otherwise non-type-hinted situations.

def do_something():
    # The set is initially empty, so no literal values to look at to have a glance at the type it holds
    keys = set()
    keys: set[int] = set()
    keys: set[int] = {/}
    keys = set[int]()

The first form gives no indication of the type of keys and type information must be derived from context. Adding a type hint results in the second form which gives me [insert your most verbose programming language] vibes :melting_face:. Readability of {/} with type hints, in the third form, is quite good, but the last form is the most concise. I’d prefer the new syntax in this case though since it gives me the least surprise (of generics being called through).

For object attributes the readability story is different (and worse with the new syntax). Attributes are often “declared”, type-hinted at the class level and possibly slotted at the same time. The default values of lists and sets aren’t set at the class level since these types are mutable, and they have to be set in __init__. The separation of type information and assignment, and the juxtaposition of empty literals hamper readability somewhat, but I can already acnticipate the types of the attributes from the type hints declared.

class Foo:
    # In effect, these are declarations of all the attributes supported
    __slots__ = ("indices", "names")
    indices: set[int]
    names: list[str]
    values: dict[str, str]

    # Some other stuff like properties and other dunders
    def ...:
        ...

    def __init__(self):
        # With the new syntax
        self.indices = {/}
        self.names = []
        self.values = {}

        # With repeated type hints
        self.indices: set[int] = {/}
        self.names: list[str] = []
        self.values: dict[str, str] = {}

        # With the current syntax
        self.indices = set()
        self.names = []
        self.values = {}

        # With explicit calls, ala configparser._ReadState.__init__
        self.indices = set()
        self.names = list()
        self.values = dict()

        # With call-through generics
        self.indices = set[int]()
        self.names = list[str]()
        self.values = dict[str, str]()

Utility wise I think the current syntax is better than others in case of attributes. Repeating type hints is just error-prone.

Overall I don’t think there’s a strong reason to adopt the new syntax, although I don’t have strong objections to it either. It is for the reason of not having strong motivations on either side that I think the impact of the new syntax should be kept as small as possible — just a syntax of a somewhat nicer way of spelling things for completeness’ sake and no more, no runtime behavior change. A lot of the current syntax is historical compromise and would probably not be designed in such ways nowadays with the resources available now (for example, the new parser has in recent years brought us nice features such as f-strings) but given how entrenched such history always turns out to become especially given the lack of strong motivation to change things, we’ll likely have to continue to support the historical legacy anyway, and any kind of runtime behavior change is likely to upset that.

This makes the most sense to me. It puts the use of {} for both dictionary and set on an equal footing. With time, the current form {} could be replaced with {:}, which would become a recommended form.

5 Likes