Proposal: Inline Type Annotations for Multiple Assignment
Abstract
This proposal suggests extending Python’s variable annotation syntax to allow annotating multiple variables in a single unpacking assignment statement.
Current Situation
Currently, to type-annotate variables in a tuple unpacking assignment, you must declare types separately:
a: int
b: int
a, b = 0, 0
Or annotate the tuple itself:
t: tuple\[int, int\] = (0, 0)
a, b = t
Proposed Syntax
Allow inline type annotations for each variable in an unpacking assignment:
a: int, b: int = 0, 0
Or with parentheses for clarity:
(a: int, b: int) = 0, 0
Motivation
1. Reduced verbosity — The current approach requires 3 lines for what is conceptually a single operation
2. Locality — Type information is co-located with the assignment, improving readability
3. Common pattern — Multiple assignment is idiomatic Python (x, y = y, x), but loses type annotation capability
4. Consistency with function signatures — Function parameters support inline annotations: def f(a: int, b: int):
That’s already possible if you use ; instead of ,:
>>> a: int = 0; b: int = 0
>>> # ^ no problem
What @khuynh22 is proposing is annotated unpacking, and that’s not possible yet. And for what it’s worth; I think that the proposed syntax (viz. a: int, b: int = two_ints) is natural, and it’s something that I’ve ran into a couple of times myself.
I think this would be fine, although the examples do show cases where explicit annotations are not necessary.
I would personally prefer something like typescript’s as that allows a value to be cast compatibly:
a, b = (0, 0) as tuple[float, float]
If we could have typed_expr := expr [as type_expr]? in the grammar (pardon the probably wrong syntax), this would be more generally useful than unpacking. For one, you could type individual tuple elements in multiple assignment:
a, b = 0 as int, 0 as float
You could also annotate individual arguments to functions or in return statements, and replace a lot of uses of type.cast() with something that’s safer and also has no runtime effect.
That points to the narrowest possible extension of the grammar. This:
(a, b): tuple[bool, str] = foo()
In the cases where I’ve wanted this, that would be totally sufficient. Usually it’s because foo() has to return Any, but we know it returns a tuple in the present case.
I also like the idea above of a cast syntax, but I’d be a little concerned that as conflicts with with statements. Could it be done with cast foo() as tuple[bool, str]?
That’s verbose but can be given unambiguous rules to compose with context managers. And verbosity isn’t terrible for a feature you probably only reach for on occasion.
Why is that a problem? For unpacking without *, I don’t understand what’s to be gained by this distinction.
To support *, this needs more thought. But if a destructuring assignment to two variables gets any annotation other than tuple, I think that needs pretty strong justification.