Allowing types being created from function calls
Hello there, I was wondering if you see in future the python type system being able to allow creating types from function call. I'm asking this because I've designed an API for a library that doesn't really work well with the type system at the moment[1]. It looks something like this: ```python def union( name: str, types: Tuple[Types, ...], * ) -> Union[Types]: # implementation is not important return None # type: ignore ``` and it can be used like so: ```python UserOrError = union("UserOrError", (User, Error)) x: UserOrError = User(name="Patrick") ``` We do this to add some information about the type (for example a name), but running a type checker on the code above gives you this error:
Illegal type annotation: variable not allowed unless it is a type alias
My question was, should I deprecate this way of creating a custom type in favour of using, maybe annotated? So the users code would look like this? ```python UserOrError = Annotated[User | Error, union(name="UserOrError")] x: UserOrError = User(name="Patrick") ``` or maybe there's some plan to support creating types from function calls (or expressions in general, when they can be statically analysed)? This looks a bit more complicated, but I don't mind it too much. [1] we made it work for Mypy via a plugin.
It's unlikely that the type system will allow the creation of types from arbitrary (user-defined) function calls. There are a few special forms where this is allowed in the type system today, such as `NewType`, `TypeVar`, and `ParamSpec`, but these are very constrained in how they can be used. There are several problems with supporting arbitrary user-defined function calls in type expressions. (By "type expression", I'm referring to an expression used in a type annotation or a type alias declarations.). 1. Evaluated types must be static, but you can pass variables to functions whose specific types are not known statically, and that can affect the return type. 2. It's important for evaluation of type expressions needs to be fast because they are used not only for type checking but also for language server features like completion suggestions. Complex expression evaluation can add tens or hundreds of milliseconds, which is not desirable for completions. 3. It's very important that evaluation of types in type expressions are deterministic across tools. This is possible for simple expression forms, but it is not guaranteed for call expressions. Different type checkers and language servers use subtly different rules and heuristics when applying overloads and solving for type variables. There are similar issues with other complex expression forms (arithmetic operators, comparison operators, comprehensions, etc.), which is why these are also not allowed in type expressions. In most programming languages, type expressions use a different (simpler) grammar than value expressions. Python doesn't distinguish between these concepts at the grammar level, but type checkers do impose limits on the expression forms that are allowed for type expressions. And there are good reasons for this. So, for all of these reasons, I think it would be a bad idea to allow function calls in type expressions. In your example above, there is already a well-defined way in the type system to define a type alias that represents a union of two types: ```python UserOrError = User | Error ``` or better yet: ```python UserOrError: TypeAlias = User | Error ``` So there's no need to invent a new mechanism. -- Eric Traut Contributor to Pylance & Pyright Microsoft
Hi Patrick, On Fri, Jun 10, 2022 at 12:42:16PM -0000, Patrick Arminio wrote:
```python def union( name: str, types: Tuple[Types, ...], * ) -> Union[Types]: # implementation is not important return None # type: ignore ```
I think the (non) implementation as shown is confusing. You surely don't actually return None? What do you do with the name, just ignore it? If you ignore the name, then it is hard for me to see why you need this union function at all. ```python UserOrError = User|Error # Or if you need to annotate it with additional information: UserOrError = Annotated[User|Error, "UserOrError"] ``` As Eric explains in another post, it is unlikely that static (compile-time) type checkers will support the evaluation and tracking of arbitrary types generated at runtime. Unfortunately subscripting syntax does not allow keyword arguments, so you cannot associate your annotation with a parameter name: ``` # This is a syntax error. Annotated[User|Error, name="UserOrError"] ``` See rejected PEP 637. -- Steve
Thank you both for the answers
```python
def union( name: str, types: Tuple[Types, ...], * ) -> Union[Types]: # implementation is not important return None # type: ignore ```
I think the (non) implementation as shown is confusing. You surely don't actually return None? What do you do with the name, just ignore it?
The implementation returns an instance of a custom defined union class,
which holds the information about the types and the name.
I think a better implementation would be to return Annotated[Union[*Types],
Something(name="passed name")],
but that would have the same issue I'm having now, since I'm passing
through a function call.
I think I can ask my users to use Annotated directly đ
My goal with a custom union function was ease of use, I wanted something
that didn't require to learn new concepted (Annotated)
and it was easier to type:
```python
UserOrError = strawberry.union((User, Error), "UserOrError")]
# vs
UserOrError = Annotated[User|Error, strawberry.union("UserOrError")]
```
but maybe Annotated doesn't look too bad, especially with the new union
syntax đ
Thanks Steven and Eric!
On Sat, 11 Jun 2022 at 00:48, Steven D'Aprano
Hi Patrick,
On Fri, Jun 10, 2022 at 12:42:16PM -0000, Patrick Arminio wrote:
```python def union( name: str, types: Tuple[Types, ...], * ) -> Union[Types]: # implementation is not important return None # type: ignore ```
I think the (non) implementation as shown is confusing. You surely don't actually return None? What do you do with the name, just ignore it?
If you ignore the name, then it is hard for me to see why you need this union function at all.
```python UserOrError = User|Error
# Or if you need to annotate it with additional information: UserOrError = Annotated[User|Error, "UserOrError"] ```
As Eric explains in another post, it is unlikely that static (compile-time) type checkers will support the evaluation and tracking of arbitrary types generated at runtime.
Unfortunately subscripting syntax does not allow keyword arguments, so you cannot associate your annotation with a parameter name:
``` # This is a syntax error. Annotated[User|Error, name="UserOrError"] ```
See rejected PEP 637.
-- Steve _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: patrick.arminio@gmail.com
-- Patrick Arminio
participants (3)
-
Eric Traut
-
Patrick Arminio
-
Steven D'Aprano