Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Samreay opened this issue Oct 25, 2023 · 7 comments
Open

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

Samreay opened this issue Oct 25, 2023 · 7 comments
Labels
topic: feature Discussions about new features for Python's type annotations

Comments

@Samreay
Copy link

Samreay commented Oct 25, 2023

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

@Samreay Samreay added the topic: feature Discussions about new features for Python's type annotations label Oct 25, 2023
@erictraut
Copy link
Collaborator

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
Copy link
Author

Samreay commented Oct 25, 2023

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
Copy link
Author

Samreay commented Oct 25, 2023

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
Copy link

kamzil commented Feb 16, 2024

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
Copy link

XieJiSS commented Mar 7, 2024

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
Copy link

wxgeo commented Mar 20, 2024

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
Copy link

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: feature Discussions about new features for Python's type annotations
Projects
None yet
Development

No branches or pull requests

6 participants