PEP 802: Display Syntax for the Empty Set

I don’t see this kind of evidence as meaningful because there is a Catch-22 effect where people are less likely to use types that are less commonly taught. When I’ve taught Python, I introduce sets at the beginning alongside the other builtin types that are of general use (ints, floats, strings, tuples, lists, and dicts). And I use sets quite frequently.

In my view, the fact that sets are conceptually basic is enough reason to give them a good shorthand, as long as it doesn’t actively interfere with learning of other basic types. The fact that it may be used a bit less is irrelevant.

As I mentioned in an earlier, post, I don’t support this PEP for other reasons[1], but I’d say the frequency of sets in tutorials or code is neither here nor there.


  1. namely that I think the proper shorthand for a set is {} and we can’t change that now ↩︎

1 Like

If I’m being honest, my impression is that the actual main motivation for adding an empty set literal is aesthetics[1]. I don’t see any real advantage that justifies adding a dedicated grammar rule for this data type.

  • Performance is definitely not the question here, even without a benchmark the difference is probably insignificant if it exists at all.
  • Readability would perhaps be improved if said literal was the logical {}, but it has already been discussed on why it is not possible to have that. Otherwise, I don’t see how set() can be beaten (and it’s only 5 characters). I’m extremely skeptical about the argument of a culture-free notation, and I don’t think that {/} is as obvious as it might seem.

If name lookup is that much of an issue as the Motivation section states, I don’t think it would be too hard for implementations of the language to have a fast path as long as set is not observed to be overridden in the environment — though I suppose I would need to justify that claim with code.


  1. I’ll admit that I am being a bit snarky here, but I don’t think consistency applies here. I don’t think that {/} is consistent with respect to other empty container literals. ↩︎

6 Likes

I feel that {{...}} is even less intuitive syntax for an empty frozenset than {/} for an empty set. And the added complexity of two new pieces of syntax for something that already exists (set() and frozenset()) just brings Python closer to Perl’s level of complexity.

3 Likes

You’re mixing up the two topics. When I mentioned {{...}} I was explaining to you why using double curly braces would not cause compatibility issues with the current syntax, not suggesting that {{...}} should be used as an empty frozenset literal.

list() == []
dict() == {}
tuple() == ()
set() == ???

It is logical that there should be an equivalent, but I don’t like the idea of special-casing the constructor call at all.

I appreciate the concern however I think this would add more confusion than it adds utility - in particular / being the division operator, it would be confusing in this context.

Considering the existing syntax, we already have set(), which is clear, obvious and in line with other types, e.g. dict(), list(). It is just 2 characters more compared to the proposal.

We also have {*[]} if you prefer a symbolic representation. The latter actually is quite nice as it is an entirely accurate representation of the exact meaning of an empty set - the unique set of no elements.

Perhaps somewhat confusing at first, but still valid is to use set({}).Of course that’s technically not needed, however it is explicit in its intent and and easy to grasp because it visibly casts{} to a set.

1 Like

(Nearly) every builtin type has a special syntax associated with it, like lists [], tuples (), dictionaries {}, integers 0, floats 0.0, strings ”” and so on. Having a special syntax for sets would only continue this pattern, and that’s a logical thing to do in my opinion. Similar to how you can define special ways the compiler treats strings with certain prefixes (b (technically not a string), f, r, t), we could add something similar, where the Parser turns d{} into a dict constructor, and s{} into a set.

We could also just have the type be some internal dict_or_set, until we can say which it is, e.g. using method access (add vs. update as an example), or key access ([…] where we use some non-integer type). If we cannot find the type, we fallback to the most used type, a dict which is generally assumed with that syntax. The only problem I could see, is users trying to make a dict, and accessing attributes that don’t exist (add, …) and getting no errors.

A few symbols have multiple meanings, () can construct a tuple or call something (or define parameters for functions / bases for classes), [] can construct a list or index some item (basically calling __get/set/delitem__ or __class_getitem__). IMO we could also have {} mean multiple things, depending on the context we use it in, or we add some string-like prefixes s and d (default would still be dict). On first glance one may think string prefixes only construct strings, forgetting b (constructor of bytes object, which is not a subclass of str nor, like str itself, a subclass of Sequence[str]. bytes is not related to str in any direct way.). Therefore I think the logical option is to make some kind of optional prefixes for {}.

5 Likes

If the type of {} was both set and dict until the magic usage heuristic insinuates a specific one then what would the behaviour of these be?

def foo(x):
    if isintance(x, dict):
        ...

foo({})

or

a = {}
a.update([(1, 2), (3, 4)])
print(a)  # Is it {(1, 2), (3, 4)} or {1: 2, 3: 4}?
14 Likes

Sets aren’t hashable so that wouldn’t be valid in the first place. But{{{/}}} would be valid–a set containing an empty frozenset. Adding more brackets would create more and more nested frozensets, switching between a frozenset and a set on the outer layer.

Let’s start over: I think an unaddressed problem with the {/} syntax is that there’s no proposed syntax for an empty frozenset. So:

  1. What would the syntax for an empty frozenset be?
  2. If we’re going to explicitly not have syntax for an empty frozenset, what happens in the future when someone says, “I think we should add XYZ as the syntax for an empty frozenset.” That is, what are the arguments against adding syntax for this?

It seems like the proponents of this PEP need to have either empty frozenset syntax OR a good reason for explicitly not having an empty frozenset syntax.

1 Like

I think a perfectly valid reason is “that’s not the scope of this PEP”. There’s no syntax for frozensets right now, empty or otherwise, and adding it would require a whole different discussion.

I don’t think it’s a reasonable requirement to say “you must either add more complexity to this PEP or argue against doing this other thing”

8 Likes

If there is a future proposal for a frozenset syntax, they might find themselves in one of two scenarios.

Either this PEP (or something like it) has been accepted, and there is some existing syntax for an empty set. In that case, the proposal for frozenset syntax would probably mirror said syntax (and their proposal must not interfere with the existing syntax).

Alternatively, they face the status quo and there is no syntax for an empty set. Then their proposal can simply say “an empty frozenset is created by frozenset(), similar to how one creates an empty set”.

1 Like

While I remain against this specific proposal, I agree that frozenset notation is out of scope.

6 Likes

If we add a symbol for empty set, I propose and much prefer {_}. It is symmetric and trivial to type (try it) and visually depicts an empty container. {-} also good and easy to type. One could be the empty set, the other an empty frozen set.

I did not see an explanation for why parse {} as 3 tokens instead of 1. How are other combinations parsed?

1 Like

An entire literal syntax for frozensets will be out of scope of this PEP. Being immutable I suppose frozensets are more often used as literal constants for things that are membership tested with in.

That said, I don’t think the current syntax is too verbose:

from collections.abc import Container

IMMUTABLE_CONTAINER: Container[str] = frozenset({“key1”, “key2”, “key3”})

...
if key in IMMUTABLE_CONTAINER:
    do_something(key)
else:
    do_some_other_thing(key)

Analogously with MappingProxyType

Similarly I don’t think we need a syntax for MappingProxyType:

from collections.abc import Mapping
from types import MappingProxyType

IMMUTABLE_MAPPING: Mapping[str, int] = MappingProxyType({"a": 1, "b": 2})

Unless we plan to do some magic in C code to accelerate the creation of immutable containers that cannot otherwise be optimized out in the future, a newfrozenset (and analogously, MappingProxyType) syntax aren’t really necessary from a readability point of view.

A strength of the letter-prefix syntax is that it would be extendable to provide a frozenset and frozendict syntax, something like f{1, 2, 3} is frozenset({1, 2, 3}), fs{} is frozenset(), fd{} frozendict().

I don’t love how it looks with collection literals, but I’m quite used to it for strings and don’t mind it there, so perhaps it softens with familiarity.

5 Likes

Might as well go fully for Rust-like macros and add a ! in there:

my_frozenset = fs!{1, 2, 3}
my_frozendict = fd!{'foo': 1, 'bar': 2}
my_empty_set = s!{}

…I started this post as a joke, but something like PEP 638 would offer a path out of the existing syntax mess in a nice way[1]. All of the containers could gain distinct macro prefixes that distinguish them from each other without needing / or something else.


  1. IMO, but I like Rust ↩︎

10 Likes

I think these are perfectly reasonable questions considering the main motivation of the PEP, because going from “built-in collection types that have a display syntax” to “built-in collection types” is not much of a stretch. If consistency justifies adding more punctuation to the language, the next step surely is adding display syntax to frozenset. Why doesn’t it have one while list, dict and set do?

2 Likes

The lack of syntax for an empty frozenset is a noticeable absence for me, so I’d rather not kick the can down the road. And part of the reason I think we don’t need syntax for an empty set are the same reasons everyone would give for not needing empty frozenset syntax.

If this new syntax improves readability, then the same arguments would apply to an empty frozenset syntax improving readability. If they don’t improve readability, then this strikes me as change for the sake of change. If they lower readability but improve performance, I’d want to the performance gains to be significant to justify an a minor loss of readability.

In all three cases, I’m not convinced the pros of adding this syntax outweigh the cons.

5 Likes

Sets are currently the only built-in collection type that have a display syntax, but no notation to express an empty collection.

Collection type yes, but it’s not the only built-in type. float is more fundamental that set, and there’s no literal for math.nan (or (-) math.inf). Unlike those, you don’t need an import to write set(). Though set() is far more common than the special floats.

Finally, this may be helpful for users who do not speak English, as it provides a culture-free notation for a common data structure that is built into the language.

It’s language agnostic, but far from culture-free – {/} / ∅ is going to be much more recognisable to those steeped in mathematics. (The same is true for set, of course, but while set is standard programming terminology for a set, the ∅ notation is rare.)


I don’t see this as a positive change overall. We go from the status quo:

  1. You can’t write an empty set in the natural way: {}.
  2. (a) Consequently, there’s no literal syntax for the empty set, so you have to use set().

to

  1. You can’t write an empty set in the natural way: {}.
  2. (b) Consequently, the syntax for the empty set is an inconsistent {/}.

Getting rid of 1 would be a nice win, but is as good as impossible. 2 (a) and 2 (b) feel like similarly minor inconsistencies. So I’d be inclined to favour the simpler status quo.

1 Like