Full disclosure: I’m not sure this is a good idea (since it is seriously cryptic when you first encounter it, and hard to look up on your own), so I have precisely zero plans to pursue it any further myself. I do, however, find it to be an intriguing idea (prompted by this post in the PEP 736 discussion thread), so it seemed worth sharing in case someone else liked it enough to pick it up and run with it.
The core of the idea:
- A standalone
@symbol on the right hand side of an assignment expression or a regular (not augmented) assignment statement with a single target becomes a shortand that means “assignment target”. Exactly what that means depends on the nature of the assignment target (more details on that below). - When the assignment target is an identifier,
@''or@""can be used to mean “the assignment target as a string” (useful for APIs likecollections.NamedTupleandtyping.NewType) - In function calls using keyword arguments,
@and@''are resolved as if the parameter name were a local variable (soparam_name=@would pass a local variable namedparam_name, andparam_name=ns[@'']would be equivalent toparam_name=ns["param_name"]
Handling different kinds of assignment targets:
- identifiers:
@is an ordinary variable lookup for that name - dotted attributes:
@looks up the corresponding attribute rather than setting it. The target expression up to the last dot is only evaluated once for the entire statement rather than being evaluated multiple times - subscript or slice:
@looks up the corresponding container subscript rather than setting it. The target container expression and subscript expression are only evaluated once for the entire statement rather than being evaluated multiple times - tuple unpacking: not allowed (specifically due to star unpacking)
- multiple targets: not allowed (which target should
@refer to? Leftmost? Rightmost? Tuple of all targets?)
Examples:
Vector = typing.NewType(@'', tuple[float, ...])
some_target = @ if @ is not None else []
call_with_local_args(some_target=@)
some.nested.attribute = @.lower()
some_container[subscript_expression()] = @.lower()
some.nested.container[x:y] = reversed(@)
running_tally = 0
running_tallies = [(running_tally := @ + len(x)) for x in items]
Disallowed due to the potential for ambiguity:
a = b = c = d = @
(a, b, *c, d) = reversed(@)
(a, b, c, d) = reversed(@) # Could potentially be allowed
a += @ # Could potentially be allowed
Note that it isn’t that there’s no reasonable meaning for these (see the various comments about that later in the thread), it’s that the use cases for them are weak and they’d necessarily be harder to interpret than the simpler cases above. If they’re left out initially, it’s straightforward to add them later. If they’re included up front and are later found to be problematic, they’re hard to get rid of.
The mnemonic for remembering this use of the @ (“at”) symbol is the acronym “AT” for “Assignment Target”. Aside from the shared symbol, it has no connection to function decorators or matrix multiplication. As a style recommendation, it would be worth pointing out that combining assignment target references and the matrix multiplication operator in the same expression is intrinsically hard to read (due to the potential confusion between the two uses).
Edit: added a clarifying note to the list of arguably ambiguous targets