Allow for multiline type-hint casting

Feature or enhancement

Something along the lines of the following

as int:
    a = 1
    b = 2
    c = 3

should compile to

a: int = 1
b: int = 2
c: int = 3

Pitch

This change would significantly clean up code in areas where types with long names are extensively used such as in physics based computational models. For example:

rvp: mmHg_s_per_mL = ...
rcs: mmHg_s_per_mL = ...
ras: mmHg_s_per_mL = ...
rvs: mmHg_s_per_mL = ...
rcp: mmHg_s_per_mL = ...
rap: mmHg_s_per_mL = ...
rav: mmHg_s_per_mL = ...
rmvb: mmHg_s_per_mL = ...
rtvb: mmHg_s_per_mL = ...

would be far cleaner written like

as mmHg_s_per_mL:
     rvp = ...
     rcs = ...
     ...

I’ve also been annoyed by this need for verbosity and like this solution. introducing the new special keyword as could case knock-on effects I’m not experienced enough to evaluate so could be a hard sell.

an alternative could be to apply the annotation across all assignments, so you could write:

rvp, rcs: mmHg_s_per_mL
rvp = ...
rcs = ...

When the need for explicit type declarations becomes a burden and a PITA, we should consider not using explicit type declarations.

I don’t know how mypy operates, but some type systems are perfectly capable of inferring types.

I argue that we should never have to declare the type of a variable bound to a constant: a: int = 1 is awful. If a = 1 of course it’s an int what else could it be?

We should only need to annotate a variable assignment if we want to loosen the inferred type, e.g. a: Any = 1. We should not have to repeat the information already there in the assignment.

(I’ll also argue that using types for physical units is the wrong solution, but that’s an argument for another day.)

But if we did decide that it would be useful to have block declarations, I suggest that there’s no need for new syntax. We could add a context manager to the typing module:

with typing.declare(mmHg_s_per_mL):
    rvp = ...
    rcs = ...

and have the static checker recognise that pattern.

One downside of this is I’m not sure how easy it would be for the context manager to populate the __annotations__ dict. Here’s a sketch of a solution:

  • if the context manager is not running in the global scope, do nothing.

  • otherwise, on entry, take a snapshot of all the global variable names: snapshot = set(globals().keys())

  • on exit, compare that snapshot to the current globals, and annotate any new variables:

    for name in globals().keys() - snapshot:
    annotations[name] = The_Type

Note that this is not thread-safe: another thread could create a global, and the context manager would then wrongly annotate it on exit. But only the main threads should create globals, so this is a very small risk.

4 Likes

Is the specific problem that you have a type whose name or definition is really long and so you don’t want to have to take up space/type out the expression for several lines? Because if so, we can already shorten type names using TypeAlias.

If the remaining problem is just needing to write the same type annotation for many closely related variables (as it looks like in your OP), I would argue a cleaner pattern is to put them in a data structure like a dict, and then you can specify the type for all of the keys at once.

I really like this suggestion, thanks for giving such a thought out answer. I think that would definitely be the way to go, but i was curious what alternatives to types you’d recommend for physical units?

Side note, the variables in our project are loaded from a file and this obfuscates the type ( which is why type hinting seemed beneficial )

I’d love if Python supports this kind of type annotation:

a, b, c: *int

to indicate that a, b and c are all ints.

Of course type checkers would need to support it too to make it useful.

I’m not sure if such extension to the syntax causes ambiguity in parsing.

The thread-safer code would be using locals:

import sys
import contextlib

@contextlib.contextmanager
def declare(type):
    try:
        locals = sys._getframe().f_back.f_back.f_locals
        snapshot = set(locals.keys())
        yield
    finally:
        locals = sys._getframe().f_back.f_back.f_locals
        for name in (set(locals.keys()) - snapshot):
            __annotations__[name] = type

with declare(int):
    a=1
    b=2
    c=3

print(__annotations__)
2 Likes

What is the downside of this simple alternative:

U = mmHg_s_per_mL
rvp: U = ...
rcs: U = ...
ras: U = ...
rvs: U = ...
rcp: U = ...
rap: U = ...
rav: U = ...
rmvb: U = ...
rtvb: U = ...

?

2 Likes

I argue that we should never have to declare the type of a variable bound to a constant: a: int = 1 is awful. If a = 1 of course it’s an int what else could it be?

If that were the case why does PEP 591 address it specifically?

Edit: Final more specially addresses that it shouldn’t be reassigned, but annotating as an int would say that a should be an int but could be reassigned to another int.

To your point Final[int] is redundant.

PEP 591 (“Adding a final qualifier to typing”) is irrelevant to my point about having to annotate variables where the type can be inferred. Annotating a variable as “Final” (i.e. a constant which should not be rebound) is not the same as annotating a value which is clearly and obviously an int as an int.

mypy correctly infers the types of a, b and c here:


a = len("hello")

b = 1234

c = b//2



def func(m:int, n:int) -> int:

    return m + n



print(func(a, b))

print(c.upper())

So that’s good.

Does anyone have any idea why sometimes Discuss inserts extra blank lines between my posts?

I always post from the same mail client using the same settings, and sometimes Discuss inserts blank lines between every line, and sometimes adds extra Ctrl-M carriage returns. It is very frustrating.