python / typing

Python static typing home. Hosts the documentation and a user help forum.
https://typing.readthedocs.io/
Other
1.6k stars 237 forks source link

PEP 692 follow up: Unpacking compatibility with dataclass/others #1495

Open Samreay opened 1 year ago

Samreay commented 1 year ago

The Unpack addition has been great, however a lot of our code has an existing library of dataclasses instead of typed dictionaries. Here's a dummy example using one of the more common patterns that will suffer from this: factory methods.

from dataclasses import dataclass

from typing import Unpack

@dataclass
class Person:
    name: str
    age: int

# ERR: Expected TypeDict argument for Unpack
def person_factory(**kwargs: Unpack[Person]):
    return Person(**kwargs)

if __name__ == "__main__":
    steve = person_factory(name="Steve", age=42)

Right now, the "fix" for us would be to duplicate the dataclass as TypedDict, but code duplication is obviously not ideal. If there's another way, please let me know, otherwise I think a really valuable enhancement to the Unpack method would be to allow it to accept other objects, such as a dataclass or a pydantic BaseModel given the popularity of pydantic and FastAPI.

Antoher use case I can think of would be to be able to Unpack[function] or Unpack[class]. Common examples here would include matplotlib and plotly, where plotting functions often expose kwargs which just get passed to a child function. That child function has all the documentation and type hinting you'd need, but its unusable unless its copied into a TypeDict (I believe). For a concrete example, the top level maptlotlib plt.plot() function takes kwargs which are passed to the Line2D class

erictraut commented 1 year ago

At runtime, kwargs is a dict[str, Any] instance, so it makes sense for its type to be a TypedDict. A TypedDict is a structural definition of dict[str, Any], and it supports all of the same operations as a dict.

I don't know what it would mean if kwargs was typed as Person as in your example above. You wouldn't be able to access it as a dataclass because it's a dict. It sounds like what you want is a way to transform a dataclass type into a comparable TypedDict class with the dataclass attributes and their types converted into TypedDict keys and value types?

I don't understand what Unpack[function] or Unpack[class] would mean.

Samreay commented 1 year ago

I don't know what it would mean if kwargs was typed as Person as in your example above.

In my mind, this would be "unpack person" aka "what is in person". Just like unpacking a typedict conceptually is looking inside the typed dict for what kwargs are acceptable, one can do the same (conceptually) for a dataclass.

I don't understand what Unpack[function] or Unpack[class] would mean.

Here's a use case I encounter all the time which makes me go to a webpage's docs instead of being able to use my IDE.

def plot_point(x: float, y: float, size: float = 1.0, color: str = "red"):
    ...

def plot_points(xs: list[float], ys: list[float], **kwargs):
    for x, y in zip(xs, ys):
        plot_point(x, y, **kwargs)

I bought up Unpack[function] (not so much as a please implement it like this) but a "this is a very common thing which doesn't have type support", and happy to know if there are alternatives available.

To just stick to matplotlib, given its the most popular visualisation library, plot passes through kwargs to Line2D. scatter passes kwargs directly to Collection. bar and barh pass kwargs directly through to Rectangle, etc.

Samreay commented 1 year ago

Sorry, missed this comment:

It sounds like what you want is a way to transform a dataclass type into a comparable TypedDict class with the dataclass attributes and their types converted into TypedDict keys and value types?

This would also make me a happy dev!

kamzil commented 9 months ago

This would be very handy when working with data (de)serialization. I'm sure many devs building on Marshmallow or Pydantic feel the same. Being able to easily convert dataclasses to TypedDicts (and back?) is a missing link in the current ecosystem.

Would be great to be able to do something like this and keep type annotations:

@dataclass
class MyClass:
    a: str

my_class_instance = MyClass(a="hello")
my_class_dict = my_schema.dump(my_class_instance) # or even better, the hypothetical dataclass_to_typeddict(my_class_instance)
my_class_dict # this should be a properly typed TypedDict instance with MyClass attributes
XieJiSS commented 8 months ago

a way to transform a dataclass type into a comparable TypedDict class with the dataclass attributes and their types converted into TypedDict keys and value types

IMO this would be really useful. For instance, pydantic models can have proper TypedDict type hints on .model_dumps() if we can transform a dataclass type into TypedDict. Currently, pydantic .model_dumps() only returns a dict[str, Any]. Also, considering we can apply @dataclass_transform to SQLAlchemy's Base class to make it behaves like a dataclass, we will be able to implement a fully-typed json_serialize function for any SQLAlchemy model objects, so that we can statically type check to see whether it fulfills the requirement of the API endpoint's expected response structure.

wxgeo commented 7 months ago

Specifying that kwargs should match the fields of a given dataclass would be very useful indeed.

My last potential usage:

@dataclass(kw_only=True, frozen=True)
class CompilationOptions:
    ...
    def updated(self, **update: dict[str, Any]) -> "CompilationOptions":  # <- Unpack["CompilationOptions"] would be nice here
        """Create an updated copy of itself."""
        return CompilationOptions(**(asdict(self) | update))

(This seems to be a recurrent request, btw. For example: https://discuss.python.org/t/typing-unpack-for-kwargs-with-self-or-dataclass-classes/43001/10 https://stackoverflow.com/questions/76939764/define-a-typeddict-from-a-dataclass)

Jerry-Ma commented 7 months ago

Hi, I was trying to do the exact them thing as the OP described, that is to obtain the TypedDict type for a Pydantic model, so that I can use that to annotate a custom constructor function:

class MyModel(BaseModel):
    a: int
    b: str

def my_init(**kwargs: Unpack[MyModel]):
    return MyModel.model_validate(kwargs)
lebrice commented 2 months ago

I tried asking for thoughts about this on the Python Typing mailing list a while back (Aug. 2022), didn't get any responses there: https://mail.python.org/archives/list/typing-sig@python.org/message/44DVY777AJXOUXE6JMJORZGL3LYIHXMF/