Prior discussion thread: PEP 802: Display Syntax for the Empty Set - #166 by gesslerpd
This post is a request for sponsors and general continuation of this specific topic.
Preamble
PEP: TBD
Title: Null Literal Unpacking
Author: Peter Gessler
Discussions-To: Pending
Status: Draft
Type: Standards Track
Python-Version: TBD
Post-History:
Abstract
We propose allowing a bare * to contribute no elements when constructing a tuple, list, or set:
tuple_ = *,
set_ = {*}
list_ = [*]
A retained null entry lets ordinary elements be added or removed without changing the container kind. The empty-set spelling and the tuple’s comma-bearing empty form follow from the same rule. Existing construction forms remain valid. Whether {*} should also become the empty set’s representation is left open.
Motivation
Adding or removing a collection element should not accidentally change the kind of object being constructed. With conventional set and tuple spellings, an otherwise local edit can do exactly that:
members = {item} # A set.
members = {} # A dictionary, not an empty set.
coordinates = () # An empty tuple.
coordinates = (item) # The item itself, not a tuple.
coordinates = (item,) # A one-element tuple.
A retained null entry makes the construction explicit independently of the number of ordinary elements. {*,} can grow into {*, item}, and (*,) into (*, item), without changing the surrounding syntax. Removing those elements reverses the edit without turning a set into a dictionary or a tuple into grouping.
Where parentheses are optional, the same rule gives values = *,, values = *, item, and values = *, item, another_item. The empty tuple then uses the same comma-bearing form as its non-empty counterparts.
This proposal addresses those transitions, not the length of set() or () in isolation. The shorter empty-set spelling is a useful consequence of a construction rule that also works for tuples and lists.
Rationale
PEP 448 allows iterable unpacking among the elements of tuple, list, and set literals. [1] We extend that model with an explicit zero-element entry: *items contributes the elements of items, while a bare * contributes none. It denotes neither None nor a new runtime value.
The distinction between {*items} and {**mapping} already identifies iterable unpacking as set construction. {*} therefore has an unambiguous meaning without changing {} or introducing a new token. Lists follow the same rule as the tuple and set forms; [] remains the usual empty-list spelling.
“One obvious way” does not require a single spelling for every value: list() and [], tuple() and (), and set(iterable) and set literals already coexist.
Null entries may occur anywhere and may be repeated, following PEP 448’s treatment of iterable unpacking. Restrictions that might catch mistakes are considered under “Open Issues”.
We prefer this generalization to an isolated empty-set notation because one rule addresses both empty-set construction and tuple-preserving edits. It does not eliminate the existing special cases; it provides a construction pattern that avoids them when used.
PEP 802 proposes {/} and a matching empty-set representation. [6] This proposal derives {*} from the shared unpacking rule; adopting it for repr as well is a separate choice discussed under “Open Issues”.
Specification
A bare * is permitted as an entry in a tuple, list, or set literal, and in unparenthesized tuple construction where starred entries are already allowed. It contributes no elements and performs no evaluation, name lookup, or iteration.
The following forms are equivalent, assuming the usual built-in bindings:
tuple_ = *,
set_ = {*}
list_ = [*]
assert tuple_ == tuple() == () == (*,)
assert set_ == set() == {*,}
assert list_ == list() == [] == [*,]
Null entries may appear in any position and may be repeated or mixed with ordinary elements and iterable unpackings:
assert (*, item) == (item, *) == (item,)
assert [1, *, *[2, 3]] == [1, 2, 3]
assert {*, 1, 1, *} == {1}
assert (*, *) == ()
The enclosing syntax determines the container kind before null entries are discarded. Thus {*} is a set, and (*, item) is a tuple even though only one element remains. A tuple-forming comma is still required: standalone * and (*) remain syntax errors, just as standalone *() and (*()) are today.
Ordinary elements and operand-bearing unpackings keep their existing evaluation order and behavior. [2] In particular, [*None] still raises TypeError. List and set literals produce fresh mutable containers; tuple identity remains an implementation detail. No particular bytecode sequence is required.
Grammar
The affected productions can be expressed as follows, omitting semantic actions and diagnostic rules. [3][5]
literal_item: star_named_expression | '*'
literal_items: ','.literal_item+ [',']
list: '[' [literal_items] ']'
tuple: '(' [literal_item ',' [literal_items]] ')'
set: '{' literal_items '}'
tuple_item: star_expression | '*'
star_expressions:
| tuple_item (',' tuple_item)+ [',']
| tuple_item ','
| star_expression
subject_expr:
| literal_item ',' [literal_items]
| named_expression
Operand-bearing alternatives retain precedence. Only these construction productions are extended; other starred-expression rules and the usual rules for nesting expressions remain unchanged.
Abstract syntax tree
The parser omits null entries from the container’s element list. No new public AST node is required. [4]
| Source | AST, omitting location attributes |
|---|---|
[*] |
List(elts=[], ctx=Load()) |
{*} |
Set(elts=[]) |
(*,) |
Tuple(elts=[], ctx=Load()) |
Context validation must precede omission so an invalid target such as [*] = [] is not accidentally accepted. Operand-bearing unpackings remain ordinary Starred nodes.
Source locations follow the usual conventions. Concrete-syntax tools may retain the original spelling; AST-only tools, including ast.unparse, need not.
Backwards Compatibility
The syntax change accepts previously invalid forms without reinterpreting valid source. Existing construction expressions require no migration; using the new syntax requires updated interpreters and parser tooling.
Some missing-operand mistakes would cease to be syntax errors. The tradeoff and possible restrictions are discussed under Additional constraints in “Open Issues”.
Changing the empty-set representation to '{*}' would have a separate compatibility cost: repr, str, and the printed forms of enclosing containers would change. Doctests, snapshots, and code expecting 'set()' could need updates, and older Python versions could not evaluate the new spelling. Retaining the current representation avoids these output changes.
Security Implications
None. Null entries execute no code and introduce no implicit calls. Ordinary elements and iterable unpackings retain their existing behavior.
How to Teach This
Beginners don’t need to learn unpacking first. A convenient starting point is to read *, as a disambiguated leading comma: keep this prefix in place and write the real elements after it.
members = {*,}
members = {*, item}
members = {*, item, another_item}
coordinates = (*,)
coordinates = (*, x)
coordinates = (*, x, y)
The star contributes no element. Inside braces, it identifies set construction rather than dictionary construction; in the tuple forms, the comma distinguishes a tuple from grouping. The punctuation stays the same for zero, one, or many elements. There is no need to switch to a constructor call when the set becomes empty or add a distinguishing comma when the tuple gains its first element.
This offers a way to teach disambiguated leading-comma notation, not a separate grammar feature: bare forms such as (, x) and {,} remain invalid, as does (*).
Unpacking concept can be introduced later through the same rule:
In a tuple, list, or set literal,
*itemscontributes the elements ofitems; a bare*contributes none.
The tutorial can present the leading-marker pattern before explaining this generalization. Existing spellings still need to be recognized when reading code, and the AST documentation should explain normalization.
Rejected Ideas
These alternatives are not adopted in this draft. Positional restrictions remain open.
Add only an empty-set spelling
PEP 802’s {/}, or a special case accepting only {*}, would solve the empty-set problem. Neither would provide the shared zero-element entry used to preserve tuple construction across edits. This PEP strives to allow avoidance of special-cases in the language and both add a new special-case. Although, special-casing {*} would keep the door open for a larger scope PEP like this to exist later.
Add operandless **
Dictionaries already have the unambiguous empty spelling {}. Adding {**} would improve symmetry but is unnecessary for the concrete set and tuple benefits.
Open Issues
Empty-set representation
Should {*} also become the representation of the empty set? This would make interactive output reinforce the new notation and align the printed form with its construction syntax.
If adopted, repr(set()) and str(set()) would return '{*}' for built-in empty sets, regardless of how they were constructed. Non-empty set representations would be unchanged. This is independent of accepting the new syntax: retaining 'set()' remains an option. The compatibility costs are described under Backwards Compatibility.
Additional constraints
The current specification allows null entries in any position and permits repetition. We could instead allow a null entry only in the first syntactic position: (*, item) would remain valid, but (item, *) would not.
This would preserve the leading-placeholder pattern and catch mistakes such as accidentally changing [head, *tail] to [head, *]. It would not catch an omitted operand in the first position: changing [*items] to [*] would still be valid.
The cost is another special case. Ordinary unpacking can occur in any element position, but its zero-element form would be allowed only first, even though its position has no effect on the result.
Allowing at most one null entry is another option. It would reject [*, *] without catching the missing operand in [head, *], and would be redundant under a first-position-only rule. Alternatively, documentation and linters could recommend leading placeholders without restricting the grammar.
The choice is between catching some mistakes and keeping the construction rule uniform. None of these restrictions would change the meaning of an accepted null entry.
Remaining decisions
The target release and coordination with PEP 802 remain to be settled. [6]
Acknowledgements
PEP 448’s generalization of iterable unpacking provides the foundation for this proposal. [1]
All contributors to the related prior PEP 802 discussion thread: PEP 802: Display Syntax for the Empty Set
References
[1] PEP 448 — Additional Unpacking Generalizations.
[2] Python Language Reference — Expressions.
[3] Python Language Reference — Full Grammar Specification.
[4] Python Standard Library — ast.
[5] CPython’s grammar and parser helpers.
[6] PEP 802.