Function annotation behavior when a function is inserted rather than a callable

Is there a reason why we’re able to have a function execute when defining a function and using a function annotation?

This snippet

def Foo() -> None:
    print("Bar")

def FooBar() -> Foo():
    return None

will print to the terminal:

$ Bar

The behavior is the same for object callables, as well:

class Foo:
    def __init__():
        print("Bar")

def FooBar() -> Foo():
    return None

will also print to the terminal:

$ Bar

Is there a use-case for inserting a function following a function annotation to call an extraneous function? I feel like it’s more of a bug, but I might be wrong.

1 Like

In Python, pretty much everything is simply defined as “an expression”. It’s extremely useful to be able to do things like List[int] which is subscripting one object with another; and since it makes sense to have that kind of flexibility, it’s not worth locking things down and putting arbitrary restrictions on the expression involved.

(There HAVE been times when restrictions have been applied, such as function decorators. That particular one got lifted recently, but others do exist. They’re rare though.)

1 Like

Annotations in their current form act like an additional expression that can be run after any line of code, e.g. The following prints Hello and then Goodbye.

foo: print('Goodbye') = print('Hello')

Currently there’s nothing special about this expression, it immediately evaluates just like any other expression in Python

For a long time it was intended that this immediate evaluation of annotations would be removed as part of "PEP 563 – Postponed Evaluation of Annotations | peps.python.org " but more recently it seems like a similar idea but a different approach to not immediately evaluate annotations might be what replaces the current behavior: “PEP 649 – Deferred Evaluation Of Annotations Using Descriptors | peps.python.org

Be careful here: what you’re doing is annotating the variable foo and also assigning it a value. It’s equivalent to this statement:

foo = print('Hello')

with the additional information that:

__annotations__['foo'] = print('Goodbye')

This will work on assignments, but not on arbitrary statements. What you’re really doing here is annotating the variable, and also initializing it, so you don’t want to have multiple annotations attached to the same variable multiple times.

Annotations are only connected to certain types of thing, mainly variables/functions in different forms, and are not actually associated with the statements that created them.

(I am a little surprised that the assignment expression is evaluated prior to the annotation. It’s unusual, in Python, for evaluation to happen in reverse order like that.)

1 Like

Oh, interesting. Perhaps a side effect of evaluating the RHS before the LHS for the ‘=‘ operator. See 7. Simple statements — Python 3.12.1 documentation, especially this:

Although the definition of assignment implies that overlaps between the left-hand side and the right-hand side are ‘simultaneous’ (for example a, b = b, a swaps two variables), overlaps within the collection of assigned-to variables occur left-to-right, sometimes resulting in confusion. For instance, the following program prints [0, 2]:

x = [0, 1]
i = 0
i, x[i] = 1, 2         # i is updated, then x[i] is updated
print(x)
2 Likes

I guess this left to right rule of the LHS hold visually for annotations also, if the annotation throws an exception the assignment still happens:

>>> i: 1/0 = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> i
1
>>> __annotations__
{}

In my reasoning of annotations I’ve always considered them to be placed “between the current line and the next line”, but maybe that was an oversimplification.

Could be. Part of my brain wants to think of annotations as comments (going all the way to the right of the executable part of the line), but if they’re written that way, they’re not evaluated at all, so that’s not really a precedent.