Proposal: Annotate types in multiple assignment

In the latest version of Python (3.12.3), type annotation for single variable assignment is available:

a: int = 1

However, in some scenarios like when we want to annotate the tuple of variables in return, the syntax of type annotation is invalid:

from typing import Any

def fun() -> Any: # when hard to annotate the strict type
    return 1, True

a: int, b: bool = fun() # INVALID

In this case, I propose two new syntaxes to support this feature:

  1. Annotate directly after each variable:
a: int, b: bool = 1, True
(a: int, b: bool) = 1, True
  1. Annotate the tuple of return:
(a, b): tuple[int, bool] = 1, True

Appendix

In other programming languages, as I know, Julia and Rust support this feature in there approaches:

  • Julia
a::Int, b::Bool = 1, true # VALID
(a::Int, b::Bool) = 1, true # VALID
(a, b)::Tuple{Int, Bool} = 1, true # INVALID
  • Rust
let (a, b): (i64, bool) = (1, true); // VALID
let (a: i64, b: bool) = (1, true); // INVALID
4 Likes

I’m pretty sure this has already been suggested. Did you go through the mailing list and searched for topics here? Without doing that, there’s nothing to discuss here. (Besides linking to them).

Secondly, try to not edit posts, but post a followup. Some people read these topics in mailing list mode and don’t see your edits.

I, for one, welcome our ants overlords am all +1 for this. I also thought this was overlooked detail from Python’s developers :slight_smile:

For reference, PEP 526 has a note about this in the “Rejected/Postponed Proposals” section:

Allow type annotations for tuple unpacking: This causes ambiguity: it’s not clear what this statement means:

x, y: T

Are x and y both of type T, or do we expect T to be a tuple type of two items that are distributed over x and y, or perhaps x has type Any and y has type T? (The latter is what this would mean if this occurred in a function signature.) Rather than leave the (human) reader guessing, we forbid this, at least for now.

Personally I think the meaning of this is rather clear, especially when combined with an assignment, and I would like to see this.

2 Likes

Thank you for your valuable response, both regarding the discussion convention for Python development and the history of this feature.

I have found a related topic here:
https://mail.python.org/archives/list/python-ideas@python.org/thread/5NZNHBDWK6EP67HSK4VNDTZNIVUOXMRS/

2 Likes

Here’s the part I find unconvincing:

Under what circumstances will fun() be hard to annotate, but a, b will be easy?

It’s better to annotate function arguments and return values, not variables. The preferred scenario is that fun() has a well-defined return type, and the type of a, b can be inferred (there is no reason to annotate it). This idea is presupposing there are cases where that’s difficult, but I’d like to see some examples where that applies.

Does this not work?

from __future__ import annotations

def fun() -> tuple[int, bool]:
    return 1, True

You don’t need from __future__ as of… 3.9, I think?

Quick question that comes into my mind: If we can infer from a function declaration what is the type of variables on the other side of = then why there is the a: bool = ... allowed in the first place? :confused:

3.10 if you want A | B too: PEP 604, although I’m not sure which version the OP is using and 3.9 hasn’t reached end of life yet.

We can’t always infer it, so annotating a variable is sometimes necessary or useful. But if the function’s return type is annotated then a, b = fun() allows type-checkers to infer the types of a and b. This stuff isn’t built in to Python and is evolving as typing changes, so what was inferred in the past might be better in the future.

So my question above was: are there any scenarios where annotating the function is difficult, but annotating the results would be easy? That seems like the motivating use case.

Would it be a solution to put it on the line above? And not allow assigning on the same line?
Then it better mirrors function definitions.

a: int, b: bool
a, b = fun()

It’s a long thread, so it might have been suggested already.

1 Like

Actually, in cases where the called function differs from the user-defined function, we should declare the types when assignment unpacking.

Here is a simplified MWE:

import torch
from torch import nn as nn

class TestModel(nn.Module):
	def forward(
		self,
		x: torch.Tensor,
	) -> tuple[torch.Tensor, torch.Tensor]:
		a = 1 + x
		b = 2 * x
		return a, b

x = torch.tensor([1.0, 2.0, 3.0])
model = TestModel()
a, b = model(x) # VALID without annotation
a: torch.Tensor, b: torch.Tensor = model(x) # INVALID

NOTE: In PyTorch, the __call__ function is internally wrapped from forward .

2 Likes

Can’t you write this? That’s shorter than writing the type annotations.

a, b = model.forward(x)

This is the kind of example I was asking for, thanks. Is the problem that typing tools don’t trace the return type through the call because the wrapping isn’t in python?

I still suggest to read the thread you linked, like I’m doing right now.

The __call__ function is not the same as forward. There might be many other preprocessing and postprocessing steps involved inside it.

1 Like

Yeah, quite a bit of pre-processing in fact… unless you don’t have hooks by the looks of it: