I'd like to chime in with an example of how PEP 563 breaks code that uses 
dataclasses.

I've written a library instant_api (https://github.com/alexmojaki/instant_api) 
that is heavily inspired by FastAPI but uses dataclasses for complex types 
instead of pydantic. The example at the beginning of the README is short and 
demonstrates it nicely. Basically it lets you write code on both the client and 
server sides that work seamlessly with standard dataclasses, type hints, and 
type checkers without any plugins, instead of untyped dicts parsed from the 
JSON that is communicated behind the scenes.

`from __future__ import annotations` breaks that README example, even though 
there are no locally defined types, because as mentioned the dataclass field 
now contains a string instead of a type.

Going a bit deeper, instant_api is powered by 
https://github.com/alexmojaki/datafunctions, which is more generic than 
instant_api so that others can build similar tools. Again, the idea is that you 
can write code with nice dataclasses and type hints, but call it with basic 
JSON serializable types like dicts. For example:

```
    from dataclasses import dataclass
    from datafunctions import datafunction

    @dataclass
    class Point:
        x: int
        y: int

    @datafunction
    def translate(p: Point, dx: int, dy: int) -> Point:
        return Point(p.x + dx, p.y + dy)

    assert translate({"x": 1, "y": 2}, 3, 4) == {"x": 4, "y": 6}

    # This is equivalent to the following without @datafunction
    # assert translate(Point(1, 2), 3, 4) == Point(4, 6)
```

In the same way as before, `from __future__ import annotations` breaks this 
code. The reason is that datafunctions itself is powered by 
https://github.com/lovasoa/marshmallow_dataclass. Here's an example:

```
    from dataclasses import dataclass
    from marshmallow_dataclass import class_schema
    
    @dataclass
    class Point:
        x: int
        y: int
    
    schema = class_schema(Point)()
    assert schema.load({"x": 1, "y": 2}) == Point(1, 2)
```

Again, in the same way as before, `from __future__ import annotations` breaks 
this code. Specifically `class_schema(Point)` breaks trying to deal with the 
string `'int'` instead of a type.

This problem was raised in 
https://github.com/lovasoa/marshmallow_dataclass/issues/13 two years ago. It's 
by far the oldest open issue in the repo. It was clear from the beginning that 
it's a difficult problem to solve. Little progress has been made, there's one 
PR that's not in good shape, and it seems there's been no activity there for a 
while. A couple of other issues have been closed as duplicates. One of those 
issues is about being unable to use recursive types at all.

marshmallow_dataclass has 266 stars. It builds on 
https://github.com/marshmallow-code/marshmallow, an extremely popular and 
important data (de)serialization and validation library. Here's a little 
timeline:

- 2013: marshmallow 0.1.0 first released in 2013
- 2014: marshmallow 1.0.0 released
- 2015: attrs (precursor to dataclasses) first released
- 2016: Python 3.6.0 final released, allowing the variable annotations which 
make pydantic and dataclasses possible.
- 2017: First version of pydantic released
- 2018: Python 3.7.0 final released, introducing dataclasses

Nowadays pydantic is the natural successor/alternative to marshmallow - Google 
autocompletes "pydantic vs " with marshmallow as the first option, and vice 
versa. But marshmallow is clearly well established and entrenched, and thanks 
to marshmallow_dataclass it was the better fit for my particular use case just 
last year when I made instant_api.

If someone wants to keep combining dataclasses and marshmallow, but without 
marshmallow_dataclass (e.g. if PEP 563 goes through before 
marshmallow_dataclass is ready) then they need to revert to the raw marshmallow 
API which doesn't use type hints. The previous example becomes much uglier:

```
    from dataclasses import dataclass
    from marshmallow import Schema, fields, post_load
    
    @dataclass
    class Point:
        x: int
        y: int
    
    class PointSchema(Schema):
        x = fields.Int()
        y = fields.Int()
    
        @post_load
        def make_point(self, data, **kwargs):
            return Point(**data)
    
    schema = PointSchema()
    assert schema.load({"x": 1, "y": 2}) == Point(1, 2)
```

This post turned out longer than I initially planned! In summary,  my point is 
that type hints and dataclasses as they work right now make it possible to 
write some really nice code - nice for humans to both write and read, nice for 
type checkers and other static analysis, and providing very nice features using 
annotations at runtime. And despite clear demand and benefits and ample time, 
people haven't managed to make this code continue working with stringified type 
annotations. Clearly doing so is not easy. So there's a good case for the 
dataclasses module to resolve these annotations to actual types, especially if 
PEP 563 goes through but even if it doesn't.
_______________________________________________
Python-Dev mailing list -- python-dev@python.org
To unsubscribe send an email to python-dev-le...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at 
https://mail.python.org/archives/list/python-dev@python.org/message/GPPWPKQ2RHABB64REWHE6HDHRNRFUXXM/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to