Similar use case as to why they are used in first place. To delay evaluation for things like recursive types. And yes I’ve written code to handle them as well. And popular libraries like pydantic have also implemented support for some quoted types. You evaluate them and allow user to register types as needed. Forward Annotations - Pydantic is an example of this.
Isn’t that solved in Python 3.14 though? From the example here:
>>> from annotationlib import get_annotations, Format
>>> def func(arg: Undefined):
... pass
>>> get_annotations(func, format=Format.FORWARDREF)
{'arg': ForwardRef('Undefined', owner=<function func at 0x...>)}
So, it should always be possible to get a ForwardRef instead of a str.
Indeed would be great if string annotations would be supported (afaict the mentioned mypy limitations are working as expected with pyright (playground – also the error message is a bit confusing as it mentions Literal)).
3.14 only “solves” this if using type expressions as type hints. The idea would be to be able to support this:
from pydantic import TypeAdapter
ta_f = TypeAdapter('MyList[int]')
MyList = list
assert_type(ta_f, TypeAdapter[list[int]])
# Currently, using `ta_f` to validate data should work, as Pydantic fetches
# annotations from the current namespace.
One possible solution without stringified types:
from pydantic import TypeAdapter
type _MyList[T] = MyList[T]
ta_f = TypeAdapter(_MyList[int])
MyList = list
assert_type(ta_f, TypeAdapter[list[int]])
This works in pyright: code sample in pyright playground.
It looks quite silly of course, but I imagine in a more realistic example, it would be more like:
type _User = User
ta_f = TypeAdapter(_User)
class User(TypedDict):
name: str
id: int
In general, I think it would be great if with Python 3.12’s lazyily-evaluated type aliases and with Python 3.14’s deferred evaluation of type annotation, that Python’s typing ecosystem can move beyond stringified types.
That’s certainly my goal, though there’s still a handful of cases where types are eagerly evaluated at runtime and quoted types may therefore be necessary (cast(), NewType(), type arguments to generic bases, the function-based TypedDict definition off the top of my head; I think there are a few others). And of course, there’s lots of existing code that uses strings in annotations, even though that should become unnecessary once 3.14 is in wide use. TypeForm will have to support code that is already in use, so it must be able to include stringified annotations.
Update:
- The second mypy PR containing TypeForm support is in review. It incorporates all of the major feedback from the first PR.
- A request for the Typing Council to provide a yes/no recommendation on the PEP is in progress.
Update: Status is unchanged:
- Still waiting on the mypy PR.
- The request to the Typing Council for a recommendation on the PEP stands.
This is probably the last time I will be posting this calendar year. I will be welcoming a new family member soon and expect to be taking a break from screens for several months.
Sorry if this has been brought up before, but why is TypeForm[Any] not equal to the literal class or subclasses of Any? It seems redundant to have both TypeForm and TypeForm[Any] mean the same thing when there is no fully supported way to type hint Any as an argument itself.
It also seems counterintuitive to me that TypeForm[Any] does not actually mean the Any type itself, when it’s kind of the whole point of TypeForm to mean the types or subtypes literally contained in it.
Edit addition:
Therefore I propose that TypeForm means any valid type literally and TypeForm[Any] means Any or subclasses of it literally. I know this differs from type and type[Any], but I think it is worth it.
Not sure that it makes sense to talk about “literal Any or subclasses of Any”. Everything is assignable to Any, so TypeForm[Any] should accept any type object at all. For what you’re looking for, I think you’d need something like a hypothetical TypeForm[Exactly[Any]], which prohibits subclasses. There’s problems with the soundness of such a type though.
Do you have an example where you’d need this behavior of TypeForm[Any]?
Some typing-like modules would benefit from such annotations. I myself made a lot of typing related stuff, and I wished for Literal[Any] (or TypeForm) often.
If TypeForm[Any] being Any or subclasses of it is too disruptive, I would also be happy with Literal[Any], as long as there is an official solution.
Update: I’m pleased to announce that mypy’s main branch now has experimental support for TypeForm!
This support will eventually make it to a released version of mypy.
Steps to experiment with mypy + TypeForm:
$ pip install git+https://github.com/python/mypy.git#egg=mypy # commit 1b7e71 or later
$ mypy --enable-incomplete-feature=TypeForm example.py
example.py:8: note: Revealed type is "type[builtins.int] | type[builtins.str] | None"
Success: no issues found in 1 source file
where example.py is:
from typing import reveal_type
from typing_extensions import TypeForm
def as_type[T](typx: TypeForm[T]) -> type[T] | None:
return typx if isinstance(typx, type) else None
typ = as_type(int | str)
reveal_type(typ)
Next steps:
- Update the “Reference Implementation” section of the PEP to note that mypy has initial support for TypeForm
- Shepherd the Typing Council to provide a yes/no recommendation on the PEP
The section “Explicit TypeForm Evaluation“ in the PEP contains this fragment:
This explicit syntax serves two purposes. First, it documents the developer’s intent to use the value as a type form object. Second, static type checkers validate that all rules for type expressions are followed:
I think it would make sense to also explicitly mention that the static type of a TypeForm(...) expression will always be TypeForm[...], even if a TypeForm wouldn’t otherwise be expected based on type context. Currently the word ‘documents’ could be understood to say that this is only intended for programmers reading the code. This can be helpful in cases where the type context empty or ambiguous. For example, "list[str]" would be inferred as a normal string literal instead of a type form without a suitable type context, and TypeForm(...) can be used to force this type context.
The typing council has discussed this PEP, and we are happy to offer our recommendation for its acceptance. Thanks @davidfstr and @erictraut for all your work on the PEP and the reference implementations!
Hey, first off thanks for the effort pushed for this PEP as this apparently falls into a use case described in this topic. Due to this I’m trying to play around with TypeForm to achieve something like this:
from typing_extensions import TypeForm, TypeVar, Protocol, runtime_checkable
@runtime_checkable
class MyProtocol(Protocol):
def method(self) -> None:
...
T = TypeVar("T", bound=MyProtocol)
def as_type(obj: object, type: TypeForm[T]) -> T | None:
return obj if isinstance(obj, type) else None
class MyObject:
def method(self) -> None:
pass
obj = MyObject()
typ = as_type(obj, type=MyProtocol)
When running the command:
mypy --enable-incomplete-feature=TypeForm typeform_example.py
I get the following output:
typeform_example.py:11: error: Argument 2 to "isinstance" has incompatible type "TypeForm[T]"; expected "_ClassInfo" [arg-type]
I’m fairly certain I’m doing something incorrectly; given the snippet above, what would be the correct approach in this case?
Types and classes are different. Every class is a type, but there are other types that are not typical classes, like Literal, union, Never, Protocol and others.
isinstance checks the class of an object. If that is what you want, you need type not TypeForm in the annotation.
You cannot simply check if something has a certain type. It requires special casing and handling all typing concepts of the language. Besides that, not even every corner of the type checking system has been fully specified, therefore those cases would even be ambiguous. Combining this makes fully type validating something very slow and incomplete.
For example:
Checking whether an object is a list[int] would require checking whether every element in the list is an int (O(n) time complexity). Doing this for deeply nested objects would be unreasonably time expensive in many cases.
You could write your own function which checks for this, or you could use a package like Beartype that does the hard work for you with is_subhint.
isinstance only works with type/Type[T] objects, but I see the variable type is declared as a TypeForm[T]. isinstance isn’t smart enough to deal with arbitrary TypeForm objects.
You need to use a runtime type checker if you want to check against a TypeForm object at runtime. Here’s how you’d do it with isassignable from trycast:
from trycast import isassignable
def as_type_form(obj: object, typx: TypeForm[T]) -> T | None:
return obj if isassignable(obj, typx) else None
Aside: That specific function is exactly equivalent to the trycast function from the trycast module, so you could just do:
from trycast import trycast
as_type_form = trycast
isinstancechecks the class of an object. If that is what you want, you needtypenotTypeFormin the annotation.
Ok I should probably reformulate with a more detailed snippet and explanation of use case:
from typing_extensions import TypeVar, Protocol, runtime_checkable
@runtime_checkable
class MyProtocol(Protocol):
@property
def my_property(self) -> int:
...
def method(self) -> None:
...
T = TypeVar("T", bound=MyProtocol)
def as_type(obj: object, type: type[T]) -> T | None:
return obj if isinstance(obj, type) else None
class MyObject:
def method(self) -> None:
pass
obj = MyObject()
typ = as_type(obj, type=MyProtocol)
This is a more complete barebone example of my situation: I want to use a protocol type to filter an object based on whether it respects that protocol or not. When running mypy against this snippet produces the following output:
typeform_example.py:28: error: Only concrete class can be given where "type[MyProtocol]" is expected [type-abstract]
As to this point I’m simply disabling the error code but I do not feel comfortable with this solution. I understand the argument of checking list[int] but in my case I’m dealing exclusively with custom-written protocols, which at best are kept in standard containers such as list, tuple. Even if the time complexity is O(n), I work in a situation where the actual n number is relatively limited (think of MyObject as a class for a device interface, hence a physical hardware object which I’m interacting with, and I realistically can’t imagine having 20 of these devices connected to my application because I’m aware of my context).
If this error code is unavoidable and I have to rely on a runtime type checker, so be it, but I wanted to make sure that this would be the last resort as I don’t want to add a dependency to my project exclusively for this situation. And naively, I imagined that this situation was relatively trivial.
You could write your own function which checks for this
I can do that but what would be a reliable approach? Using hasattr? What if there’s an external plugin that provides a protocol inheriting from MyProtocol?
i.e.
# this protocol is defined in another module/external library
from typing import runtime_checkable
from mylib import MyProtocol
@runtime_checkable
class AnotherProtocol(MyProtocol):
@property
def another_property(self) -> str:
...
def another_method(self) -> str:
...
You need to use a runtime type checker if you want to check against a
TypeFormobject at runtime. Here’s how you’d do it withisassignablefrom trycast
I was under the impression that trycast works only with TypedDict, does it extend to protocols as well?