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

Add alias as a field() parameter for dataclasses #101192

Open
tmke8 opened this issue Jan 20, 2023 · 7 comments
Open

Add alias as a field() parameter for dataclasses #101192

tmke8 opened this issue Jan 20, 2023 · 7 comments
Assignees
Labels
stdlib Python modules in the Lib dir type-feature A feature request or enhancement

Comments

@tmke8
Copy link
Contributor

tmke8 commented Jan 20, 2023

Feature or enhancement

Add an alias parameter to dataclasses.field() which behaves like alias in attrs.field() (see here and search for "alias"). It has the effect that it changes the corresponding argument name in the synthesized __init__.

Pitch

Since attrs 22.2.0, attrs.field has an alias parameter that does the following:

from attrs import define, field

@define
class C:
    _name: tuple[str, str] = field(alias="name")

    @property
    def full_name(self) -> str:
        return " ".join(self._name)

c = C(name=("John", "Smith"))  # `name` not `_name`
print(c.full_name)

The typical use-case for this is private instance variables as in the above example. But there could be other use-cases as well, like

@define
class C:
    viewport: tuple[int, int] = field(alias="vp")

C(vp=(2, 4))  # constructor uses shorthand

Besides being a useful feature in and of itself, another reason to add this to the standard library is that it would bring dataclasses in line with dataclass_transform which already supports the alias parameter in field(): https://peps.python.org/pep-0681/#field-specifier-parameters . It is currently a bit strange that dataclass_transform has a feature that dataclasses.dataclass doesn't actually have.

Previous discussion

https://discuss.python.org/t/add-alias-as-a-field-parameter-for-dataclasses/22988/1

@tmke8 tmke8 added the type-feature A feature request or enhancement label Jan 20, 2023
@ericvsmith ericvsmith self-assigned this Jan 20, 2023
@ericvsmith
Copy link
Member

I’ve got this partially working. I’ll investigate attrs to make sure we’re in sync. Once I get a PR, hopefully some typing folks can take a look.

@AlexWaygood AlexWaygood added the stdlib Python modules in the Lib dir label Jan 20, 2023
@vvzen
Copy link

vvzen commented Jun 21, 2023

Another use case could be to allow doing breaking changes when one wants to rename the spelling of a property.
For example, given a class like this:

@dataclass
class SubmissionData(object):
    allowlist: list[str]

If I wanted to rename the spelling of allowlist to be allow_list without breaking existing client code, I could just alias allowlist to allow_list , say like this:

@dataclass
class SubmissionData(object):
    allow_list: list[str] = field(alias="allowlist")

Then, only in a major version of my package, I would drop support allowlist completely, etc.
Thanks!

@tmke8
Copy link
Contributor Author

tmke8 commented Jun 23, 2023

Note though that when you define an alias only the alias name will be available in the __init__, so people would not be able to use the new name allow_list in the constructor.

@padrepitufo
Copy link

that seems to break the definition of "alias" for me. I wonder if we really have to be 1:1 with attrs.

@tmke8
Copy link
Contributor Author

tmke8 commented Oct 28, 2023

Hmm, my main use case would be private attributes where you wouldn't want to use the non-alias name in the constructor:

@dataclass
class C:
    _name: str = field(alias="name")

but I guess there might be other use cases. Though if you want to keep both names in the constructor, it would get a bit complicated. You'd need code for dealing with the case where both names are used at the same time, for example.

I agree though that the word "alias" sounds like both names will be available. Unfortunately it's already enshrined in PEP 681...

@user293811
Copy link

user293811 commented Feb 8, 2024

Hey I was wondering if there've been any updates on this. While doing some Googling I saw ran into this forum where @ericvsmith had volunteered to work on this in January 2023. Wondering if it's been abandoned.

I have a use case where I need custom setters for type checking purposes. As a result I have:

@dataclass
class Struct:
    _field1: pd.DataFrame
    _field2: pd.DataFrame
    _field3: pd.DataFrame

    @property
    def field1(self):
        return self._field1

    @data.setter
    def field1(self, val):
        someguard(val)
        self._field = val
   ...

but when I initialise I have to do,

struct = Struct(_field1=value1, _field2=value2, _field3=value3)

which is inconsistent with how I access members:

a = struct.field1
b = struct.field2
field3 = pd.DataFrame(a+b)

One work around is to have a custom __init__ where I map field1 to self._field, but this doesn't work with dataclass.replace.

It would look a lot cleaner if I could just use field1, field2, field3 instead of having to use both field1, field2, field3, _field1, _field2, _field3 depending on the circumstance.

@GalaxySnail
Copy link
Contributor

There is a workaround with mypy. For details: https://web.archive.org/web/20240109161017/https://florimond.dev/en/posts/2018/10/reconciling-dataclasses-and-properties-in-python

tl;dr:

from dataclasses import dataclass, field

@dataclass
class Vehicle:

    foo: str
    wheels: int
    _wheels: int = field(init=False, repr=False)

    @property  # type: ignore[no-redef]
    def wheels(self) -> int:
        print("getting wheels")
        return self._wheels

    @wheels.setter
    def wheels(self, wheels: int) -> None:
        print("setting wheels to", wheels)
        self._wheels = wheels

It works well with dataclass.replace:

>>> a = Vehicle("bar", 42)
setting wheels to 42
>>> print(a)
getting wheels
Vehicle(foo='bar', wheels=42)

>>> from dataclasses import replace

>>> b = replace(a, wheels=12345)
setting wheels to 12345
>>> print(b)
getting wheels
Vehicle(foo='bar', wheels=12345)

>>> c = replace(a, foo="baz")
getting wheels
setting wheels to 42
>>> print(c)
getting wheels
Vehicle(foo='baz', wheels=42)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stdlib Python modules in the Lib dir type-feature A feature request or enhancement
Projects
None yet
Development

No branches or pull requests

7 participants