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 annotation