Is `Annotated` compatible with `type[T]`?

The tightening of the spec is more about keeping the future of the Python type system sustainable, than solve individual user’ problems in the short term. If type checkers diverge too much because of unspecified or underspecified behavior a lot of time is wasted on user-submitted bug reports pulling the various implementations in opposite directions. It’s also impossible to measure how compliant a type checker is without a solid spec and conformance test suite, and you can only really write that test suite if you have a spec to go by.

The transition from no/very loose spec to a more formal spec will probably be painful for some users, since some behavior they’ve come to rely on had to be changed. But in the long term I think it will end up being a win for everyone, because it’s now easier to find and fix problems in the spec, while at the same time providing the authors of type checkers with the tools to remain compliant, so turnover should be quicker.

But for now we have accrued a lot of technical debt, and that will take its time to sort out. The typing council has been doing a good job so far and making decent progress nailing down ambiguities in the spec for some very core concepts in the type system like tuples, named tuples and enums. I don’t think it’s particularly productive to squander that forward momentum on the basis of a few short term breakages, especially if there is a workaround for the short term that gets rid of the noise[1].

  1. and that workaround almost always exists courtesy of Any, you lose out on some type safety in the short term, but I think that’s a small price to pay ↩︎

The change improves type safety in functions that take type arguments. If you receive an argument of type type, it should be safe to do things with it that you can normally do with types, such as passing it as the second argument to issubclass(). However, Annotated[int, ...] is not valid there.

I do agree that it’s worth it for type checkers to be conservative in making changes here, at least until TypeForm is available. Mypy’s behavior is already very inconsistent here though, so it would be hard to justify retaining this exact behavior:

from typing import Annotated, TypeVar

T = TypeVar("T")

def takes_type(t: type[T]) -> T:
    return t()

AnnotatedOptInt = Annotated[int | None, ""]
takes_type(AnnotatedOptInt)  # Error

AnnotatedInt = Annotated[int, ""]
takes_type(AnnotatedInt)  # No error
takes_type(Annotated[int, ""])  # Error
1 Like

We have longstanding tests in that assert that Annotated[int, "metadata"] is callable, so I don’t think this is accidental behaviour. If you want to change this in CPython, it would require a two-version deprecation period as per PEP 387: if the deprecation were introduced in Python 3.13, the feature could potentially be removed in Python 3.15.

I’m not suggesting that we change it in CPython, just that type checkers flag it as an error. There is plenty of precedent for type checking behavior to be more restrictive than runtime behavior.

Well, earlier in the thread you did float the idea of opening a bug report at CPython asking for this behaviour to be changed :slight_smile:

Fully agreed — the main point I wanted to make, though, was that it didn’t seem likely to me that the runtime behaviour here was accidental. It’s had this behaviour since the PEP was originally implemented at runtime by the PEP author (first in typing_extensions, then later in CPython) and the PEP author added a test to specifically assert this behaviour.

Thanks everyone for your feedback. The proposed clarification to the typing spec has been accepted by the Typing Council.

1 Like

We’ve updated Pydantic to use Any for non type[T] types: Make TypeAdapter's typing compatible with special forms by adriangb · Pull Request #8923 · pydantic/pydantic · GitHub.

We look forward to a TypeForm PEP getting passed.