5

I just stumbled across this weird thing with python 3.12.

I have a type I import under if TYPE_CHECKING:.

When trying to reference this type without quotes, I expect a NameError, because the variable BaseModel doesn't exist at runtime.

But for some reason python lets me use this name as "bound" in 3.12 generic syntax. I didn't expect python to not error on an undefined BaseModel variable in why_does_this_work.

from typing import TYPE_CHECKING, TypeVar


if TYPE_CHECKING:
    from pydantic import BaseModel


def works_as_expected[T: "BaseModel"](a: type[T]) -> T:
    return a()


# ???
def why_does_this_work[T: BaseModel](a: type[T]) -> T:
    return a()


# Breaks as expected:
# NameError: name 'BaseModel' is not defined
_T = TypeVar("_T", bound=BaseModel)

def whatever(a: type[_T]) -> _T:
    return a()

Can someone explain this to me?

Is there a way to inspect this T in why_does_this_work at runtime, try to get it's "bound" type and, by doing so, trigger a NameError?

I know about PEP-649 and PEP-749 - deferred evaluation of annotations, but these PEPs are getting implemented in 3.14 and I found this behavior in 3.12.

1
  • You actually don't need the import at all. I remember this being in the 3.14 changelog, but I don't know why it's working here. It must be something special about type parameter lists? Commented Sep 27 at 15:14

1 Answer 1

4

PEP 695 describes the behaviour your are witnessing, which was implemented in Python 3.12

This PEP introduces three new contexts where expressions may occur that represent static types: TypeVar bounds, TypeVar constraints, and the value of type aliases. These expressions may contain references to names that are not yet defined. ... If these expressions were evaluated eagerly, users would need to enclose such expressions in quotes to prevent runtime errors. ...

To prevent a similar situation with the new syntax proposed in this PEP, we propose to use lazy evaluation for these expressions, similar to the approach in PEP 649. ...

Emphasis mine. So yes, the bound of your type variable is lazily evaluated. You can do the following to force an evaluation, and it will generate a NameError, saying that BaseModel does not exist.

typing.get_type_hints(why_does_this_work)['return'].__bound__
# or for the argument `a`
typing.get_type_hints(why_does_this_work)['a'].__args__[0].__bound__

NB. It is only when you inspect the bound of T that the error occurs (ie. when you fetch the __bound__ attribute). You can freely get the annotation for argument a and the return annotation without causing an error.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.