PEP 802: Display Syntax for the Empty Set

I can’t recall for sure whether this came up while I was still BDFL, but I have a vague feeling that it did and that I rejected it out of hand. I still agree with the naysayers in this thread – the cure (including any of the “Rejected proposals”) is worse than the disease.

  • The analogy with ∅ is hard to see – if someone asked me “how would you spell ∅ using only ASCII characters” I would never come up with {/}, and if someone asked me “what does {/} mean” I wouldn’t immediately guess the empty set (I would eventually by a process of elimination, I suppose, but non-language-designers would not think that way).
  • No matter what you try, there’s no orthogonality to be had here – {} is plain ambiguous, being the natural empty edge case for both dicts and sets, by analogy to empty tuples and lists. It means the empty dict by fiat (because dict notation predates set notation by 20 years), and people have been taught this for 15 years (the modern syntax for sets was introduced in Python 2.7 and 3.1, 2010, after a brief mention in PEP 3100).
30 Likes

As above, I think performance is less of a motivator. However, looking at the details:

We save two instructions and a name lookup:

PS> .\python.bat
Running Release|x64 interpreter...
Python 3.15.0a0 (remotes/upstream/HEAD-1-g71075ef1c9e-dirty:71075ef1c9e, Aug  8 2025, 19:28:32) [MSC v.1942 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import dis
>>> dis.dis('{/}')
  0           RESUME                   0

  1           BUILD_SET                0
              RETURN_VALUE
>>> dis.dis('set()')
  0           RESUME                   0

  1           LOAD_NAME                0 (set)
              PUSH_NULL
              CALL                     0
              RETURN_VALUE

On my PC, I get 79ns for a no-op lambda, 153ns for set() and 122ns for {/}.

microbenchmarking code
import statistics
import timeit

NUMBER = 10**7
LOOPS = 5
S_TO_NS = 10**9

def bench(f, desc):
    times = timeit.repeat(f, repeat=LOOPS, number=NUMBER)
    avg_time_s = statistics.mean(times) / NUMBER
    avg_time_ns = avg_time_s * S_TO_NS
    print(f'Average: {avg_time_ns:.0f}ns; {desc}')

bench(lambda: ..., 'no-op')
bench(lambda: set(), 'set()')
bench(lambda: {/}, '{/}')
5 Likes

I’m not convinced there would be any FLUFL-induced ambiguity. <> <> <> would mean “empty set not equal to empty set”. More generally, the parser always knows whether it’s expecting an expression or an infix operator.

4 Likes

A possible way out would be for {} to produce a special object that can
be either a dict or a set, and morphs into one or the other depending on
how it’s used for the first time.

3 Likes

I don’t think readability impacts of the new syntax have been discussed enough in the PEP.

Empty sets are often created outside a loop, or as an attribute, and then added to inside the loop, on each method call, or in other repeating constructs. Since sets are usually created only once I don’t think the performance gain is much of an issue (I expect the hashing, when items are actually added, to take far longer than the name lookup of set). A cursory search of = set() in the stdlib reveals that the variable name of choice often includes one of the following words like “keys”, “names”, “done”, “seen”, or sometimes more specific, descriptive ones. Some of these names clearly apply to sets, but some names are also applicable to other container objects. In these more ambiguous cases, keeping the syntax of empty sets distinct from an empty dict is quite important especially if the type information is not apprarent from the context. An explicit set() feels more readable to me than {/}, especially when juxtaposed with other empty container literals.

This readability story changes with the use of type hints. As an example, the attributes of _ReadState in stdlib configparser are type-hinted. If lines 568–570 of configparser.py

With the new syntax the code may read like this:

...
# Attribute type hints at class level

def __init__(self):
    self.elements_added = {/}
    self.errors = []

Imagine if another attribute, self.cursect, were designed to no longer default to None but to take an empty dict as the default. {/} [] {} juxtaposed together will be harder to decipher. Fortunately in this case I already know the types to expect of the attributes from the type hints at the class level, lessening the readability impact.

The motivation of the PEP points to being beginner-friendly, but if readability-wise the new syntax is only better with type hints and not otherwise, then I don’t think the motivation really stands in this case.

5 Likes

Given that a dict is a set of ordered pairs, Greg’s idea of {} == empty set-or-dict is interesting. A subclass of both set and dict would come close, inheriting the methods of both. Logically, both are specializations set-or-dict, but method inheritance dictates the direction of subclassing. But I would still prefer to leave well-enough alone.

2 Likes

This is modelled after the corresponding mathematical symbol ‘\emptyset’.

Hmm, why not just ?

We have a little gain with {/} over set() for interactive work (three vs four symbols). While source code is written once but read many times.

3 Likes

Note that, in the Motivation section, the PEP says:

An empty set cannot be constructed with {}; this literal constructs an empty dictionary.

This can be confusing for beginners, especially those coming to the language from a scientific or mathematical background […]

However, even with the {/} symbol introduced, the confusion (if any) still exists because {} still represents a dict rather than a set.


Also in the Motivation section of the PEP:

Finally, this may be helpful for users who do not speak English […]

IMHO this reason is pretty weak.


Depends on the context. If I were told to draw ∅ with ASCII letters, I would do (/). However, since Python and mathematicians already use {} for sets, I think the analogy is actually quite obvious and is easy to teach/learn.


I expect performance to be discussed at least as a “side-effect”, since this is what people talk about the most (aside from readability) for [] vs list() / () vs tuple() / {} vs dict() discussion. [0] [1] [2] [3]


Overall, +0 on this. I think {/} is suitable for scenarios where I have to modify the hard-coded set frequently. But honestly, I rarely do this. I believe the PEP would be more compelling if it showed some numbers on how frequently a hard-coded empty set appears in real-world code.

5 Likes

I don’t think this would help as much as you hope. You don’t want to know how many times set() appears in real code, you want to know how often the author would prefer to use {/} instead of set(). I am pretty sure the latter is a far lower number than the former.

5 Likes

Instead of introducing a new syntax, I’d just prefer sticking with set(), for the reasons people have outlined above. There should be one– and preferably only one –obvious way to do it.

23 Likes

I’m fairly positive on {/} on a conceptual level. It’s neat, concise, and would be relatively easy to add to the language. However, it would take a lot to overcome the other issues that seem more broadly applicable.

It’s unlikely that performance would ever be a persuasive argument. The value of writing it as {/} rather than {*()} would be minuscule currently, and it could end up being equivalent in the future, given continued work on the JIT. Comparing to set is less fair, it usually ends up needing a global name lookup, and that cannot be optimized out as easily.

On learnability and rememberability, this feels like it is the kind of thing someone learns once, and then it sticks with them; However, even with all sorts of preexisting learning materials being updated for this(and not all materials are digital and are more costly to update), I believe exposure to it’s use would be many people’s first time seeing this. In that scenario, IDEs and the surrounding context are likely to provide enough information that those people figure it out just fine, but it is worth keeping in mind.

Something I was pleasantly surprised to be wrong about: I thought that {/} would also be nearly unsearchable in a traditional search engine[1], but I checked in with a few different search engines, and even with it only being a PEP, this thread is already visible without scrolling. In fact, this set of symbols currently has better search results than {*()}, though I wonder how that would change if both were actually in use rather than one being currently discussed on a site with exceptional indexing.

If linters start recommending this for any reason[2], it would create churn without much benefit beyond the case that those linters should already catch, namely, shadowing, but the impact would either be low, or there is a surprising amount of set() use that is not represented here.


As to existing options that work even in a case of shadowing, {*()} seems fine enough to me. While it is one of the ugliest string of symbols I’ve ever written in Python, I have written this before. The only question I received during review was why we couldn’t fix the shadowing, not what it was doing or why it was done. The answer is potentially relevant to people wondering how the shadowing could occur. We didn’t control the interface, and it is understandable that someone might shadow set in a class body; The convention of postfix underscore to avoid shadowing a builtin causes a lot of code to look more like a mistake[3] upon reading when it’s a method (foo.set_(...))


  1. Yes, a grep of the docs would find it trivially as well, reasonably assuming it was documented. ↩︎

  2. Performance, avoidance of shadowing, opinionated style that happens to be widely used and loudly opinionated, etc. ↩︎

  3. While I have seen exactly one intentional shadowing of set that I can recall offhand, the accidental case happens often enough with set due to it also being a verb that I have an expressible preference that someone write something like set_value, set_state, etc., when used as a verb naming a method or function. It also rarely creates an issue given the scope of the shadowing. ↩︎

3 Likes

Got sent here from the r/programming thread and honestly this feels like a solution in search of a problem. I’d love for this to be accepted because new syntax is cool but IMO apart from that, there really isn’t a good reason for this change.

6 Likes

Create and use a new token for ‘{/}’

There have been previous proposals to create a new token for this construct. This would require ‘{/}’ to be written literally, with no whitespace between the characters.

We insted chose to allow whitespace between the brackets and the slash, treating {, /, and } as three distinct tokens, as we cannot see any reason to prevent it. However, we expect that the vast majority of uses will not include whitespace.

I was originally going to pick on this part and suggest we should use that new token to keep it simple and ensure there was only one way to do things. Because I never want to see it split across lines or with whitespace in the middle. But I don’t have strong enough feelings to argue for that as I could come up with a use case for splitting it across lines when code is used as configuration:

PEPS_WITH_SYNTAX_IN_THIS_ALLOWLIST: set[int] = {
    /
}

When you want to encourage or require something be maintained as a multi line one item per line set even when currently empty.

I never want to see it split across lines or with whitespace inside in any other situation though so i’d be happy as a clam for it to be a single {/} token. But no autoformatter like ruff or black would ever be silly enough to allow anyone to leave whitespace inside one of these empty set literals so that “problem” solves itself either way.

Why do I +1 support the notion of this syntax at all? Because I’ve been irritated since the dawn of the set type that there was no way to write one that did not involve an expensive name lookup and function call at runtime. That mental leap every time I use a set literal of understanding that the type changes when I take the last element out and I need to change the initialization syntax is annoying. I’ve seen this lead some authors (this was not pre-set-literal <=2.6 code) to using set([some, constant, literal]) in their code instead of using our actual non-empty set syntax just to avoid inconsistency.

I don’t care that we can/could/do in some special circumstances detect this scenario and optimize our bytecode to actually have a literal instead of the name lookup and call. Those are magic special cases that cannot be relied upon. It is far easier to explain the language to anyone that “when you see a non keyword string anywhere, that is always a name, which you should assume means one or more dict accesses or method calls” along with “whenever you see () after a name, that is a function call”. set() magically not doing either is a surprising thing to understand in the dynamic language defined by simple rules context even though we all secretly want such expressions to be optimal behind the scenes. dedicated syntax like {/} is clearly not a name lookup or function call, the reader can just know it’ll be as fast as possible no matter what. even when running in an environment where someone’s poorly authored test has decided to mock.patch(“your_module.set”, …).

10 Likes

what does the FLUFL think about adding a new unambiguous empty dict {:} literal while they’re in there? :stuck_out_tongue:

12 Likes

Note, that you’ve used (likely due to autocomplete) an en-dash (–), but what you’re quoting is intended to be an em-dash (—), this is because the author uses -- to represent em-dashes but this is uncommon in the U.S where --- tends to be preferred. Also in the U.S. it is preferred to not use any spaces either side of the em-dash but in this quote spaces are used two different ways, and in another part of the quote a different way of putting spaces around the em-dash is used.

What was that about one obvious way? Perhaps this line was meant as tongue in-cheek and not intended to be used as a counterpoint in discussions?

I read most the arguments against an empty set literal to be close to tautological. It seems a lot can be reduced to “It doesn’t exist now so I don’t have use for it”, perhaps if it had always been there you would have found yourself using it?

On the other hand, a lot of the arguments for it appear to be about personal usage, I would be a lot more convinced, and I’m sure others to, if there are examples of empty set literals from other languages that are well used and liked by that languages community.

3 Likes

Actually the latest interpreters have optimizations for exactly this case. Search the cpython sources for _Py_Specialize_LoadGlobal and LOAD_GLOBAL_BUILTIN.

9 Likes

I don’t think there’s a strong need to change the repr() of the empty set to the new literal. That the repr() of the empty set is "set()" has been well-established, and if anyone were to process the repr results programmatically (not that relying on repr() output is the recommended thing to do), they’d have to change their code (which may not be Python, but can also be shell script, embedded C, etc.) as well.

2 Likes

I don’t think you’ve read the arguments that closely, I’m afraid. In particular, I’ve never claimed that - what I’ve said is that I prefer the existing form, and I see no reason to change. Adding a new form should, in theory, be irrelevant to me, but social dynamics being what they are, it won’t be - instead, there will be pressure to use the new form because (insert any of the arguments given in the PEP, none of which convince me, personally).

In spite of your (somewhat amusing) dismissal of the “there should be only one way” zen, it does actually encapsulate an important principle of the language and its community, that we prefer code to look relatively uniform rather than being idiosyncratic, and we encourage use of common forms even when there are alternatives. That’s the key problem I have with this proposal - it’s not going to leave those of us who are happy with the existing form unaffected. If someone could assure me (convincingly) that it would, then I’d be neutral on the proposal (well, -0, as I mentioned before). But I’m not convinced that will be the case.

10 Likes

That happens plenty of other times in Python though. If you are parsing repr output and relying on it being stable, you are going to run into problems. Remember, there was a time before ANY set display syntax existed - obviously the current repr wasn’t valid then. All the way up to Python 2.7, we had:

>>> {1,2,3}
set([1, 2, 3])
>>> set()
set([])

If there’s dedicated syntax for an empty set, I would be quite surprised if it were NOT used in the repr, as this would suggest that the syntax isn’t fully supported by the language.

3 Likes

Since you asked :smile: - I actually don’t mind either {:} or {,}. The former is a little weird because colons aren’t used in set literals with elements, which I guess leans me a little more toward {,}[1].

The reasons for rejecting {,} in the PEP don’t bother me so much since we already have literal notations for empty tuples and lists[2]. Using {/} to rhyme with ∅ seems a little obscure and definitely looks odd to me.

I’ve never been that unhappy with set() though, but I can see the appeal to having a literal for empty sets.


  1. and it’s way too late to make {} mean empty sets and {:} to mean empty dicts, even if that seems much more natural to me ↩︎

  2. single element tuples still being the oddball here ↩︎

11 Likes