python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.26k stars 2.79k forks source link

Add function signature hooks for dataclasses functions: replace, asdict, astuple #5152

Open ilevkivskyi opened 6 years ago

ilevkivskyi commented 6 years ago

Currently, the plugin only supports dataclass creation (i.e. generation of dunder methods with correct types, including __init__). However, some functions in the dataclass modules have quite broad types in typeshed, for example:

def replace(obj: _T, **changes: Any) -> _T: ...

so that arbitrary arguments can be give for changes, etc. The plugin should be able to give more precise type for these functions. In particular, asdict could return a TypedDict.

danr commented 3 years ago

How would one go about to fix the type signature for replace? In TypeScript it is possible to give a replace function this type:

function replace<X extends Record<string, any>>(base: X, changes: Partial<X>): X {
  return {...base, ...changes}
}

It's not a 100% correspondence, I'm using TS' Record type instead of dataclass and just one single changes argument instead of a **changes keyword argument. But in essence it's the type I want to give replace from the dataclasses module.

edaqa-uncountable commented 3 years ago

This hole is allowing errors to leak through in some of my production code. Lacking a 'typeof' operator as well, it's difficult to ensure the types are correct.

OlegAlexander commented 1 year ago

Hello and thank you for creating mypy!!

If I may, I'd like to make a case for this issue. dataclasses.replace() is equivalent to the with keyword in F#. For anyone wishing to do typed functional programming in Python, having replace() checked at compile time is critical. This issue was opened 5 years ago and even the mypy documentation says:

Some functions in the dataclasses module, such as replace() and asdict(), have imprecise (too permissive) types. This will be fixed in future releases.

I can only assume that this isn't a trivial issue to fix.

It seems I haven't made a strong case for this issue after all--I've only managed to express my frustration 😢. It's just that the next time someone asks me if it's possible to do typed functional programming in Python, I'd love to say "Yes!" instead of "Almost!"

ikonst commented 1 year ago

With asdict returning an anonymous TypedDict (as planned in #8583), would this bring us any closer to closing this issue?

If the TypedDict was not anonymous, and better yet if it was part of dataclasses (e.g. if a dataclass would have __TYPED_DICT__ and in particular __PARTIAL_TYPED_DICT__ that's total=False) then this could perhaps be done in the type definition a'la

def replace(obj: _T, **changes: _T.__PARTIAL_TYPED_DICT__) -> _T: ...

Otherwise we could try something like #14526 where we determine the signature in a function sig hook. Curiously while in attrs.evolve we can take the __init__ signature and mutate it to be all-ARG_NAMED_OPT, for replace we must make init=True arguments ARG_NAMED since they must be explicitly re-specified during replace.

ilevkivskyi commented 1 year ago

FWIW I remember I thought #8583 was an OK solution. But we also need to support other methods: astuple() and replace(). They should be quite straightforward to implement, it is just a matter of spending time on carefully writing the code.

OlegAlexander commented 1 year ago

dataclasses.replace works properly since mypy 1.5.0. Thank you for fixing this!!