Revisiting inline type annotations for multiple assignment

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):

8 Likes

It wouldn’t be consistent with function signatures.

You give as an example:

def f(a: int, b: int):

If you add defaults, that becomes:

def f(a: int = 0, b: int = 0):

This is not consistent with:

a: int, b: int = 0, 0

In order to be consistent, it would have to be:

a: int = 0, b: int = 0
1 Like

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.

9 Likes

In your idea, would this also apply to chained assignments?

a: int = b: int = 42

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.

1 Like

Generally agree that being able to do this would be great.

But have no favourite solution.

a: int, b: int = 1, 2          # 1.

a, b: tuple[int, int] = 1, 2   # 2.

a, b = 1, 2 as tuple[int, int] # 3.

(3) is my least favourite though.
There is : for annotations, would be good to find a way to use it here as well.

1 Like

That’s true! Maybe the point about consistence with function is not correct anymore, but I would still love the ability to do what I purpose tho

FWIW: in Rust you can write

let (a, b): (u32, u32) = (1, 2);

(playground)

So the 2nd syntax comes closest (although there’s also something to be said for requiring parentheses like in Rust).

3 Likes

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.

5 Likes

One small problem: (a, b) is not a tuple. It’s unpacking syntax. The return value of foo() has a type. (a, b) doesn’t.

A cast syntax makes more logical sense. In a redundant, telling the type checker something that it already knows, sort of way.

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.