gesslerpd
(Peter Gessler)
May 21, 2026, 2:00pm
231
Haha, yeah I can see that, note this proposal only applies to right hand side contexts. Maybe best way to think about it is in a repl with *().
My proposal would work same way using that which works today, but shortened to “*” compiled out of the resulting AST and possibly limited to first literal element position.
But maybe the compiler should have a special case which compiles out the *() idiom starred node inside literal containers completely, then get all the benefits without meaningfully changing language (but doubt anyone would learn this if not official syntax).
Edit: Have put up a issue/PR that optimizes the {*()} idiom regardless of any decision on this PEP or other related proposals. Very early on @AA-Turner had mentioned this optimization.
opened 10:27PM - 21 May 26 UTC
closed 10:38AM - 22 May 26 UTC
type-feature
interpreter-core
pending
# Feature or enhancement
### Proposal:
Today, `ast.unparse(ast.Set(elts=[]))` … renders as `{*()}`, because Python has no dedicated empty-set literal. Without AST canonicalization on parse, reparsing that output produces a `Set` containing `Starred(Tuple([]))` instead of an empty `Set`, so the AST does not round-trip cleanly.
Regardless of decision on [PEP 802](https://discuss.python.org/t/pep-802-display-syntax-for-the-empty-set/101676) this may provide a win for anyone who has already adopted this `{*()}` idiom.
### Has this already been discussed elsewhere?
I have already discussed this feature proposal on Discourse
### Links to previous discussion of this feature:
Various contributors to the discussion on PEP 802: https://discuss.python.org/t/pep-802-display-syntax-for-the-empty-set/101676
- https://discuss.python.org/t/pep-802-display-syntax-for-the-empty-set/101676/7
- https://discuss.python.org/t/pep-802-display-syntax-for-the-empty-set/101676/231
### Linked PRs
* gh-150205
main ← gesslerpd:literal-null-unpack-idiom
opened 10:30PM - 21 May 26 UTC
## Proposal
When parsing a container literal whose only element is a direct `… *()`, simplify that element out of the AST during preprocessing.
Proposed examples:
```python
ast.parse('[*()]', mode='eval').body # List(elts=[])
ast.parse('{*()}', mode='eval').body # Set(elts=[])
ast.parse('(*(),)', mode='eval').body # Tuple(elts=[])
```
The main motivation is full `ast.unparse()` symmetry for the empty-set idiom.
Today, `ast.unparse(ast.Set(elts=[]))` renders as `{*()}`, because Python has no dedicated empty-set literal. Without AST canonicalization on parse, reparsing that output produces a `Set` containing `Starred(Tuple([]))` instead of an empty `Set`, so the AST does not round-trip cleanly.
With this change:
```python
empty_set = ast.Set(elts=[])
ast.parse(ast.unparse(empty_set), mode='eval').body
```
would produce the same `ast.Set(elts=[])` structure again.
Applying the same rule to `[*()]` and `(*(),)` keeps the behavior consistent across the three container literal node types.
## Scope
This proposal is intentionally narrow.
Only simplify the direct lone `*()` case:
- `[*()]`
- `{*()}`
- `(*(),)`
- assignment forms such as `x = *(),`
Do not simplify larger literals such as:
- `[*(), 2]`
- `{*(), 2}`
- `(*(), 2)`
- `[1, *(), 2]`
- `{1, *(), 2}`
- `(1, *(), 2)`
- `[*(), *()]`
That keeps the rule cheap, obvious, and tied to the empty-container idiom rather than introducing a broader AST rewrite.
## Why this belongs in AST preprocessing
This behavior is better handled as AST canonicalization than as a special case in `ast.unparse()` tests or in `assertASTEqual()`.
- It gives one canonical AST for a syntax that is operationally empty.
- It fixes real round-tripping for `ast.unparse(ast.Set(elts=[]))`, not just one test helper.
- It also benefits other AST consumers such as `compile(..., PyCF_ONLY_AST)`, `python -m ast`, and `ast.literal_eval()`.
- It avoids teaching `assertASTEqual()` ad hoc equivalence rules for specific syntax patterns.
In other words, the parser would produce the normalized tree once, and every downstream consumer would see the same result.
## Compatibility and behavior
Runtime semantics do not change.
The visible AST change is that tools would no longer see `Starred(Tuple([]))` for the lone forms above; they would instead see empty `List`, `Set`, or `Tuple` nodes.
That is an externally visible AST change, but it is narrow and maps a degenerate form to the canonical empty container node.
Notable consequences:
- `ast.unparse(ast.parse('{*()}', mode='eval').body)` remains `{*()}`
- `ast.unparse(ast.parse('[*()]', mode='eval').body)` becomes `[]`
- `ast.unparse(ast.parse('(*(),)', mode='eval').body)` becomes `()`
- `ast.literal_eval('{*()}')` can evaluate to `set()` once the parsed AST is canonicalized
* Issue: gh-150204
2 Likes