You nerd-sniped me there. :-)

I think this is perhaps too complicated to attempt to make it all work.

- We intentionally don't support things like `isinstance(x, List[str])`
because that would require checking all the items with `isinstance(item,
str)`, and that seems a speed trap. Reverting this decision would be hard
work.

- `isinstance(3, float)` returns False, because at runtime int and float
are distinct types, even though static type checkers regard int as a
subtype of float (with some caveats, at least in mypy's case, but that's
basically how it works). Changing this behavior at runtime will break tons
of existing code that tries to distinguish ints from floats but checks for
float first (since everyone "knows" they are distinct types), so this would
be even harder to get through than the previous bullet.

- Currently static type checkers don't allow defining any methods (even
class methods) in TypedDict instances, so you can't manually an
`__instancecheck__` method to a TypedDict class. (But we could add it to
typing.TypedDict of course.)

- There's also the issue that bool is a subtype of int. Again, very hard to
change that without breaking code.

- Some static type checkers (mypy, but not pyright -- haven't tried others
yet) disallow `isinstance(x, SomeTypedDict)` -- presumably because they are
aware of the problems above.

Probably the best you can do is write your own recursive
isinstance-lookalike that has the behavior you need for validating JSON.
But then you're no better off than any other JSON validation framework (and
I expect there already to be some that introspect TypedDict subclasses).

I suppose you could come up with some mechanism whereby you can create a
parallel hierarchy of classes that do support isinstance(), so you could
write e.g.

ICircle = make_interface(Circle)
IRect = make_interface(Rect)
# etc.

def draw_shape():
    match request.json:
        case ICircle(center, radius): ...
        case IRect(x, y, width, height): ...
        ...

but this loses much of the original attractiveness.

--Guido

On Sat, Nov 21, 2020 at 10:46 PM David Foster <davidf...@gmail.com> wrote:

> I am excited about the potential of the new PEP 634-636 "match" statement
> to
> match JSON data received by Python web applications. Often this JSON data
> is
> in the form of structured dictionaries (TypedDicts) containing Lists and
> other primitives (str, float, bool, None).
>
> PEP 634-636 already contain the ability to match all of those underlying
> data
> types except for TypedDicts, so I'd like to explore what it might look
> like to
> match a TypedDict...
>
> Consider an example web application that wants to provide a service to draw
> shapes, perhaps on a connected physical billboard.
>
> The service has a '/draw_shape' endpoint which takes a JSON object
> (a Shape TypedDict) describing a shape to draw:
>
>      from bottle import HTTPResponse, request, route
>      from typing import Literal, TypedDict, Union
>
>      class Point2D(TypedDict):
>          x: float
>          y: float
>
>      class Circle(TypedDict):
>          type: Literal['circle']
>          center: Point2D  # has a nested TypedDict!
>          radius: float
>
>      class Rect(TypedDict):
>          type: Literal['rect']
>          x: float
>          y: float
>          width: float
>          height: float
>
>      Shape = Union[Circle, Rect]  # a Tagged Union / Discriminated Union
>
>      @route('/draw_shape')
>      def draw_shape() -> None:
>          match request.json:  # a Shape?
>              ...
>              case _:
>                  return HTTPResponse(status=400)  # Bad Request
>
> Now, what syntax could we have at the ... inside the "match" statement to
> effectively pull apart a Shape?
>
> The current version of PEP 634-636 would require duplicating all the keys
> and value types that are defined in Shape's underlying Circle and Rect
> types:
>
>          match request.json:  # a Shape?
>              case {'type': 'circle', 'center': {'x': float(), 'y':
> float()}, \
>                      radius: float()} as circle:
>                  draw_circle(circle)  # type is inferred as Circle
>              case {'type': 'rect', 'x': float(), 'y': float(), \
>                      'width': float(), 'height': float()} as rect:
>                  draw_rect(rect)  # type is inferred as Rect
>              case _:
>                  return HTTPResponse(status=400)  # Bad Request
>
> Wouldn't it be nicer if we could use class patterns instead?
>
>          match request.json:  # a Shape?
>              case Circle() as circle:
>                  draw_circle(circle)
>              case Rect() as rect:
>                  draw_rect(rect)
>              case _:
>                  return HTTPResponse(status=400)  # Bad Request
>
> Now that syntax almost works except that Circle and Rect, being TypedDicts,
> do not support isinstance() checks. PEP 589 ("TypedDict") did not define
> how
> such isinstance() checks should work initially because it's somewhat
> complex
> to specify. From the PEP:
>
>  > In particular, TypedDict type objects cannot be used in isinstance()
> tests
>  > such as isinstance(d, Movie). The reason is that there is no existing
>  > support for checking types of dictionary item values, since isinstance()
>  > does not work with many PEP 484 types, including common ones like
> List[str].
>  > [...]
>  > This is consistent with how isinstance() is not supported for List[str].
>
> Well, what if we (or I) took the time to specify how isinstance() worked
> with
> TypedDict? Then the match syntax above with TypedDict as a class pattern
> would work!
>
> Refining the example above even further, it would be nice if we didn't
> have to
> enumerate all the different types of Shapes directly in the
> match-statement.
> What if we could match on a Shape directly?
>
>          match request.json:  # a Shape?
>              case Shape() as shape:
>                  draw_shape(shape)
>              case _:
>                  return HTTPResponse(status=400)  # Bad Request
>
> Now for that syntax to work it must be possible for an isinstance() check
> to
> work on a Shape, which is defined to be a Union[Circle, Rect], and
> isinstance()
> checks also aren't currently defined for Union types. So it would be useful
> to define isinstance() for Union types as well.
>
> Of course that match-statement is now simple enough to just be rewriten
> as an
> if-statement:
>
>          if isinstance(shape := request.json, Shape):
>              draw_shape(shape)
>          else:
>              return HTTPResponse(status=400)  # Bad Request
>
> Now *that* is a wonderfully short bit of parsing code that results in
> well-typed objects as output. 🎉
>
> So to summarize, I believe it's possible to support really powerful
> matching
> on JSON objects if we just define how isinstance() should work with a
> handful
> of new types.
>
> In particular the above example would work if isinstance() was defined for:
>      * Union[T1, T2], Optional[T]
>      * T extends TypedDict
>      * Literal['foo']
>
> For arbitrary JSON beyond the example, we'd also want to support
> isinstance() for:
>      * List[T]
>
> We already support isinstance() for the other JSON primitive types:
>      * str
>      * float
>      * bool
>      * type(None)
>
> So what do folks think? If I were to start writing a PEP to extend
> isinstance()
> to cover at least the above cases, would that be welcome?
>
> --
> David Foster | Seattle, WA, USA
> Contributor to TypedDict support for mypy
> _______________________________________________
> Python-ideas mailing list -- python-ideas@python.org
> To unsubscribe send an email to python-ideas-le...@python.org
> https://mail.python.org/mailman3/lists/python-ideas.python.org/
> Message archived at
> https://mail.python.org/archives/list/python-ideas@python.org/message/Y2EJEZXYRKCXH7SP5MDF3PT2TYIB7SJS/
> Code of Conduct: http://python.org/psf/codeofconduct/
>


-- 
--Guido van Rossum (python.org/~guido)
*Pronouns: he/him **(why is my pronoun here?)*
<http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-change-the-world/>
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/AZ2JBLZGMA4ZIDTTC5J3F55XZTV43HOS/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to