Keep some way to put anything into annotation (in the future version of Python)

Summary:

I would like future versions of python to have some easy way to put anything into annotation, not just Types or stringified types. It is possible now, but I see it is going to be deprecated in the future.

Currently I can write:

def f(): return 1

class A:
    a: f() = "and now for something completely different"

And annotation for "a" would return a proper callable. With from __future__ import annotations it would return a string, which could be hard to parse.

Maybe allow something like

from __future__ import old_annotations

to retain the current behaviour?

Motivation

I had developed a library that allows to write declarative statements in Python, in the style of probabilistic programming. It uses synaxis like

    class MyModel(Model):
        x: normal_prior(mean=0, std=1) = 0
        y: normal_prior(mean=3, std=1) = 3
        z: gamma_prior(a=3, b=10) = 3

Here normal_prior would return a callable. Annotation describe prior for the model parameter, rather than its type (which is mostly irrelevant in my context).

The library works very well (in my context), by bringing expressivity and brevity of declarative probabilistic programming where it is needed. I use it to run different modelling projects, including official COVID model for Finland. The problem is, it look like future version of Python would not support such a frivolous use of annotations. So I suggest keeping the possibility to opt-out of new behaviors, maybe something like

from __future__ import old_annotations

or some alternative to get_type_hints, which would run eval in the appropriate context, but would not required to return Type.

I think PEP 649 would suit your purposes? Along with inspect.get_annotations.

The difference between PEP 649 and the current behavior I think is that the annotations would be evaluated lazily when accessed rather than immediately.

There are discussions already about PEP 649 vs. PEP 563 (i.e. making from __future__ import annotations default). This I think is the latest one: Type annotations, PEP 649 and PEP 563

I don’t think there is appetite to keep compile level flags around that change the behavior of Python around permanently. Generally __future__ imports are meant to have a specific version of Python when they will become default and the behavior switching is removed.

Would inspect.get_annotations return the same thing that current __annotation__ have? If it is, this satisfy my request.

I think you made a mistake in your example:

class A:

Second attempt. Hopefully this time Discuss won’t just cut four fifths
of my post.

I think you made a mistake in your example:

class A:

would set the annotation to the result of calling f(), not to f. To set
the annotation to the function itself, you need:

class A:

Under from __future__ import annotations the annotation f will be
recorded as a string, “f”. You should use:

typing.get_type_hints(A)['a']

which will automatically resolve the string back to the real function
object. See PEP 563 for details:

Although PEP 563 has been accepted, there is still debate going on
whether the decision should be reversed and PEP 649 accepted instead:

Third attempt! (I’m about ready to give up on Discuss. Its email
interface is impossibly buggy.)

The second attempt was better, except that Discuss cut out the relevant
annotation lines from the classes. I have no idea why, but I’m going to
guess that it doesn’t like the ellipsis dot dot dot. Or who knows?

I think you made a mistake in your example:

class A:

would set the annotation to the result of calling f(), not to f. To set
the annotation to the function itself, you need:

class A:

The rest of the post made it through in one piece, but just in case, the
important part is that if you are using from __future__ import annotations
you can use

typing.get_type_hints(A)['a']

to resolve the string to the actual function object f.

Ah crap, so it’s not the ellipsis. If this fails, I give up.

The lines that Discuss keep cutting are

  1. 4-space indent a colon space f left-parenthesis right-parenthesis space equals quote text quote

  2. 4-space indent a colon space f space equals quote text quote

Or (and hopefully this won’t be cut)

Line 1 a: f() = "text"

Line 2 a: f = "text"

I will probably regret wading into this, but PEP 484 has the @no_type_check decorator which you can place on a class or function (you have to import it from typing though, oh irony :-).

I think the OP meant to have the annotation be the return-value of f.


OT; are you encoding code as indented or fenced?

1 Like

Im aware of different PEPs related to the type hinting, this is why I’m writing my suggesting. I would like to retain the current behaviour, which new PEP may not support.

Let me use another example.

class A:
    x = 42
    y: x = "and now for something completely different"

Imagine I would like to have 42 in the annotations for A.y. Why I would like to do this is another question. The use case I have is allowing declarative notation for model description. This works great for me! Maybe some other creative soul will come out with other uses? Anyways, this is absolutely how it works now

class A:
    x = 42
    y: x = "and now for something completely different"
print(A.__annotations__)  # {'y': 42}

Not so much in the future

from __future__ import annotations   # imitate PEP 563
from typing import get_type_hints

class A:
    x = 42
    y: x = "and now for something completely different"

print(A.__annotations__)  #" prints {'y': 'x'}
print(get_type_hints(A)['y'])  # builtins.NameError: name 'x' is not defined

With somehow easier example, where at least I can use eval myself

from __future__ import annotations   # imitate PEP 563
from typing import get_type_hints

x = 42
class A:
    y: x = "and now for something completely different"

print(get_type_hints(A)['y'])  # builtins.TypeError: Forward references must evaluate to types. Got 42.

@no_type_check does not solve my problem

from __future__ import annotations  # imitate PEP 563
from typing import get_type_hints, no_type_check

x = 42
@no_type_check
class A:
    y: x = "and now for something completely different"

print(A.__annotations__)  #" still prints {'y': 'x'}
print(get_type_hints(A))   # prints {}

If PEP 659 will pass through, it seems I will have no problems. Put if PEP 563 will go on, it would be nice to have some way to opt out of it.

If you want to keep the current behaviour even if and when annotations
are automatically stringified, you probably won’t be able to. The point
of future imports is to manage the transition between two different,
incompatible syntaxes, not to allow a choice between then forever. So I
doubt there will be a choice between

from __future__ import annotations  # stringify annotations
from __past__ import annotations # don't stringify annotations

or any other such syntax.

I think that your use-case is a good argument in favour of reverting PEP
563 in favour of 649. I think the Steering Council is, or was, looking
for evidence that people are using annotations for purposes other than
typing.

But if PEP 563 goes ahead, what you could do is write a class decorator
that automatically unstringifies the annotations on class creation:

@unstring
class A:
    x = 42
    y: x = 43

giving {‘y’: 42} for A.__annotations__.

Looking more closely at get_type_hints, I suspect that it may not
be the right tool for the job you want. It does too much work (walking
the whole MRO of your class), and is too strict with its insistence that
annotations resolve to types.

I don’t have a good alternative to get_type_hints. Maybe I’m being
over-cautious, but I fear calling eval() on arbitrary strings, although
I see that get_type_hints calls eval, so perhaps it makes no difference.

If you came up with such a decorator, I would like to see it added to
the typing module.

Maybe inspect.get_annotations, added in 3.10?

I was going to suggest the use of Annotated[..., my_special_annotation_value] from PEP 593, but then I realized you have to put some real typing-based annotation at the ... location. :thinking:

Perhaps you could put Any at the ... as a placeholder? It’s not a great solution since it probably has side-effects during type-checking such as (probably) disabling type inference for the related value, but I’m guessing you’re not interested in running a type-checker over this code at all.