Intrastellar Explorer
Intrastellar Explorer

Reputation: 2391

How to make dataclass instance attribute non-public and an __init__ arg?

If I want an instance attribute to be:

Normally, I would do this:

class Foo:
    def __init__(self, bar: str):
        self._bar = bar

foo = Foo(bar="bar")  # foo.bar would raise an AttributeError

However, in dataclasses, I'm unsure how to do this.

from dataclasses import dataclass

@dataclass
class Foo:
    bar: str  # This leaves bar as a public instance attribute

What is the correct way to do this in dataclasses.dataclass?

Upvotes: 3

Views: 5100

Answers (3)

martineau
martineau

Reputation: 123423

If you want it to be an __init__() argument, just write your own which will prevent one from being automatically generated. Note that specifying init=False as shown below isn't really required since it wouldn't have happened anyway, but nevertheless seems like good way to draw attention to what's going on. For the same reason, specifying field(init=False) for the private _bar field is superfluous.

from dataclasses import dataclass, field


@dataclass(init=False)
class Foo:
    def __init__(self, bar: str):
        self._bar = bar

    _bar: str = field(init=False)


foo = Foo(bar="xyz")
print(foo._bar)  # -> xyz
print(foo.bar)  # -> AttributeError: 'Foo' object has no attribute 'bar'

Upvotes: 1

Roman Zh.
Roman Zh.

Reputation: 1049

Here you can find a big discussion on using @property in dataclasses which can resolve your problem (the _bar is safe under the setter/getter).

There could be some problems with unwanted attributes displayed as the output of __repr__ or asdict(), which can be solved as here


PS I can't add a comment, so I've added a new answer.

Upvotes: 1

chepner
chepner

Reputation: 530960

This is a relatively simple case for an InitVar and the __post_init__ method. (Though the verbosity is probably not what you had in mind.)

from dataclasses import dataclass, InitVar


@dataclass
class Foo:
    bar: InitVar[str]

    def __post_init__(self, bar):
        self._bar = bar

This behaves as you described. bar (as written) is a required argument to __init__, but does not make an attribute named bar.

>>> f = Foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() missing 1 required positional argument: 'bar'
>>> f = Foo(9)
>>> f._bar
9
>>> f.bar
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Foo' object has no attribute 'bar'

To be clear, this is something you would use in an existing dataclass; you wouldn't choose to create a dataclass just to do this.

Upvotes: 8

Related Questions