Data Classification: Does Python still have a need for class without dataclass?
blog.glyph.im
blog.glyph.im
Whether you're using Kotlin data class, a C# record, a Scala case class, or whatever, we've seen in practice that these are only good for (often immutable) public state and data composition. Business objects with more logic and more than simple state/member manipulation are only hampered by dataclass field approaches. If Python embraced immutability and composition more at its core, then using dataclasses in these cases would make more sense.
write your own `__init__` method then, as you would anyway. Dataclass' default init is way better than that of `object`.
> advanced field defaults
same
> no equality
set eq=False, all dataclass methods are optional
> custom repr/str
write __repr__ and __str__ methods, same as init, dataclass' default is much nicer than that of `object`
> advanced inheritance
What's the issue with inheritance? what's "advanced" inheritance, you mean mixins?
> etc dataclasses give you nothing but a bunch of magic to turn off or work around.
Python `object` has all kinds of default behaviors too, which are just worse. default behaviors aren't "magic". dataclasses currently manufactures its methods at runtime, so that's maybe "magic", but all of that could be built into Python, then it would just be, "default behavior".
This video has some good explanations of inheritance behavior in python https://www.youtube.com/watch?v=X1PQ7zzltz4
I don't think anything is particularly magical. At most you have a `blah=False` or you just override the thing manually and the decorator 'just works'.
Maybe some really insane inheritance stuff? IDK
I haven't run into any case where I didn't end up wanting `@dataclass`
I don't teach dataclasses until I teach intermediate classes because I like my students to understand decorators beforehand.
This make me wonder if I should experiment with teaching dataclasses instead of __init__ (and just hand-wave decorators and types)...
Sure technically (the best type of correct) it is not a constructor.
Where would this be important? Maybe it could be taught as part of "language lawyer" course.
I teach OOP around lesson 10. Each lesson is about 2hours.
Complete beginnners to programming are able create basic classes and even inheritance.
Some people like OOP some stick with imperative style. Some migrate to more functional style (which I personally advocate).
Going through this old thread: https://stackoverflow.com/questions/6578487/init-as-a-constr... puts me firmly into "this is hair-spliting" camp.
FAANG interviews?
Knowing that __new__() comes before __init__() is probably useful, as the deeper one gets into a language the more important it is to understand the lifecycle of an object. Otherwise, the only thing I can think of that's special is understanding that and how it works as a singleton.
In short this is irrelevant for those interviews. It’s definitely relevant if you do python as a day job.
data Point3D(x, y, z) from Vector: ...
is a terrible syntax, there are plenty of legitimate use cases where you'd have an overwhelming amount of fields in a class/structure, and listing them line by line (as well as explicitly specifying accessors to handle the couple special cases) would end up much more readable. I'd rather see something more along these lines: data Parrot(Animal):
name
age: int
alive: bool = False
purchased_in: Shop | None = field(default_factory=get_shop)
...
(Move "field" to builtins, etc.)But overall this proposal seems like a no-brainer, even before the addition of the match statement. The "classic" __init__ is a jarring violation of DRY, baked right into one of the most common idioms in the language, and gets in the way of not just the "QoL" features like destructuring or type checking, but also of the efforts to build more efficient implementations that could pre-allocate object memory without speculative analysis, or optimise the dict lookup away.
The question is not "shall we", but "why wasn't it done in 2008".
And not only that, I think there are many reasons to want to continue having non-dataclass classes. Dataclass fits best with record-type data and not with encapsulated data or classes with mostly private attributes.
Trivial getters and setters, __init__ methods that copy their parameters to members of the same name, and other boring Java-like patterns are certainly served well by convenience tools like the attrs library or dataclasses, but distinguishing records from serious classes requires significant skill.
I think slots do that, don't they?
Even just one would help us evaluation your claim!
> Dataclass fits best with record-type data and not with encapsulated data or classes with mostly private attributes.
How does it "not fit"? How would you know if a class were a datalclass? Would it break something?
For me, not having to write a constructor is a small win right away, having the class have a useful `str` representation is nice, automatically comparable and hashable are nice, I can also make immutable classes which is very nice, etc, etc.
Unless you really have no constructors of visible members, what's the drawback?
I don't use `dataclasses` everywhere but more and more when I start any class I make it a `dataclass`.
1. pydantic by default ignores extra fields - it's useful when I make an API call and want to extract and validate only certain fields from the response, while dataclasses throws errors if you don't specify all fields, and this behavior can't be disabled
2. pydantic is more customizable - I can overwrite the BaseModel if I have a custom need
3. pydantic seems to integrate better with python typing - I'm not sure how to explain this one, but it feels more natural and dynamic
I'm not sure what the performance concerns are, though
[1] https://docs.pydantic.dev/usage/models/#creating-models-with...
This is like -- there are many libraries that do PCA, from sklearn to statsmodels to plain scipy to genomics tools. But in a glue-language workflow you should choose a tool for its semantics.
data Point3D(x, y, z) from Vector:
is never, ever, ever, ever gonna happen. It's ugly, alien to the rest of python style, will be a massive pain to implement in the lexer/parser, adds another keyword and mechanism, and most importantly, makes the word "data" unusable as a variable name. Dataclasses are "just" decorator functions (there is some special optimization and meta magic under the hood, but still just a callable).There's a glaring reason why we can't "just make all classes dataclasses" - it would break the entire data model of the language. "class" keyword is basically syntactic sugar for the "type" builtin. Everything (with a few small exceptions) you touch in python is a subclass of object. Changing the behavior of class to by default behave like dataclass would have knock-on effects across the entire language on every single python3 codebase on the planet.
Ok, even assuming we could surmount the insurmountable breaking changes, would we even want default classes to be dataclasses? Not in the slightest. Python may not have true private variables, but libraries/APIs leverage dunders, properties, and hidden state to do all kinds of OOPy things. You can't do that with dataclasses. Many times you have objects bound to attributes which don't have concrete types, such as endpoints in HTTP frameworks and anything with dynamic dispatch (yeah there's Callable, but it is tricky to type check).
Dataclass also has a self-documentation effect: it says "this thing is kind of like a record". Not a primitive, not a function, not a router, view, thread, tree node, god-class or stack frame. If I see dataclass, I almost always expect to be able to de/serialize it to some sort of message. Complex state, anything unpickleable, or exotic types don't really belong in a dataclass.
The entire modules typing, os, sys, struct, inspect, json.
Anything which is a metaclass or relies on metaclass behavior (you have to be careful combining dataclass with other metaclasses, it's possible but with footguns akimbo).
Datetime actually should be a dataclass, but that api is so crufty and unintuitive that it's never gonna happen, and trying to shoehorn it to a dataclass would break oodles of libraries and applications.
threading: Thread, Lock, Rlock anything whichs uses locks
Numpy.ndarry, pandas.Series, pandas.Dataframe, torch.Tensor. Numerous scikit-learn models.
Probably the vast majority of c extensions.
pydantic.BaseModel, fastapi.FastAPI, fastapi.APIRouter (same with flask/django), celery.Task.
I could go on but I think I've made my point. There are plenty of non-dataclass use cases out there.
I maintain another dataclass-like library[1] that's written fully as a C extension. Moving this code to C means these types are typically 5-10x faster for common operations[2]. It'd be nice if the builtin dataclasses were equally performant.
[1]: https://github.com/jcrist/msgspec
[2]: https://jcristharif.com/msgspec/benchmarks.html#benchmark-st...
If you run this code:
import dataclasses
@dataclasses.dataclass
class Base:
a: int
b: int = 1
@dataclasses.dataclass
class Child(Base):
c: int
Then you'll get this error: TypeError: non-default argument 'c' follows default argument import dataclasses
@dataclasses.dataclass
class Base:
a: int
b: int = 1
@dataclasses.dataclass(init=False)
class Child(Base):
c: int
if you want the init to be generated for you, you have to tell it how, given that "b" has a default value. so "c" needs to be keyword only here: import dataclasses
@dataclasses.dataclass
class Base:
a: int
b: int = 1
@dataclasses.dataclass
class Child(Base):
_: dataclasses.KW_ONLY
c: intdoes not work:
from pydantic import dataclasses
@dataclasses.dataclass
class Base:
a: int
b: int = 1
@dataclasses.dataclass
class Child(Base):
c: int
error File "pydantic/dataclasses.py", line 265, in pydantic.dataclasses.dataclass
...
File "/opt/python-3.10.0/lib/python3.10/dataclasses.py", line 539, in _init_fn
raise TypeError(f'non-default argument {f.name!r} '
TypeError: non-default argument 'c' follows default argument
it works with KW_ONLY though awkward that pydantic.dataclasses doesn't supply it directly, need to split up imports among pydantic.dataclasses and dataclasses.`slots=True` should also be the default.
While it doesn’t solve reading other people’s code with the defaults you’d like to have, dataclass being a decorator and not a special syntax means that you can just derive a new decorator from it with the defaults you want.
This could hurt basic static analysis.
That’s a good point. I know some of the python static analysis tools can “see through” some simple indirection, but don't know if even a simple wrapper injecting arguments would work with any (or which) of them for this purpose.
I am partial to a dataclass keyword but… it’s tough. The problem with declarative stuff like this is that sometimes you really just want to do things in a certain order and now you have to communicate that (the keyword argument hack is an example of this)
It's not very ergonomic, but if you start from that assumption you could have ad-hoc syntactic support that desugars to a limited version of an optics library.
What’s an example of a language that does this by default?
frozenmap (PEP 603) is in draft.
About 10 years ago someone who I thought was pretty smart said it was because dots are nicer to type, fair enough I guess. It just always seemed silly to me to go through setting up a class but not getting the benefits of using methods.
def foo(...):
...
return yhat, sse, aic, bic
yhat, _, _, _ = foo(ytrue)
plot(ytrue, yhat)You only have to specify them 'up front' in terms of how the code is laid out. But in terms of which order you write the code in, you can add them as you need them.
Specifying the names and types is extremely useful for readers (and users) of your code.
https://github.com/python-attrs/attrs/blob/main/src/attr/_ma...
class Food:
def __init__(self, temp):
if str(temp).lower().strip() in ['hot', 'warm', 'cold']:
self.temp = temp.lower().strip()
else:
try:
temp_int = int(temp)
if temp_int > 120:
self.temp = 'hot'
elif temp_int < 60:
self.temp = 'cold'
else:
self.temp = 'warm'
except ValueError:
raise ValueError(f'temp must be a number or one of the following: "hot" "warm" "cold", received: {temp}')This is a based on code I have in my editor right now. Typing straight into the comment box so it might be wrong.
Toy = Literal['toy']
class Spec(BaseModel):
sample_size = Union[float, Toy]
def __post_init__(self):
self.sample_size = 0.1 if self.sample_size == 'toy' else self.sample_size import dataclasses
from typing import Literal
@dataclasses.dataclass
class Food:
temp: Literal["hot", "cold", "warm"] = dataclasses.field(init=False)
temp_arg: dataclasses.InitVar[str | int]
def __post_init__(self, temp_arg):
# optional bonus, get the list of temps from the annotation at runtime
# possible_temps = self.__annotations__["temp"].__args__
if str(temp_arg).lower().strip() in ["hot", "warm", "cold"]:
self.temp = temp_arg.lower().strip()
else:
try:
temp_int = int(temp_arg)
if temp_int > 120:
self.temp = "hot"
elif temp_int < 60:
self.temp = "cold"
else:
self.temp = "warm"
except ValueError:
raise ValueError(
f"temp must be a number or one of the "
f'following: "hot" "warm" "cold", received: {temp_arg}'
)Seems way more verbose and less readable.
the previous example was not verbose enough IMO, the intent here is clear and the two variables with different purposes are kept separate
> allowing variables to be statically typed is a good idea that reduces bugs
Not when it comes at the cost of readability. I'd argue more bugs (and definitely more severe bugs) are caused by poor readability than by lack of static types.
Having each of these classes have a clearly defined type-annotated constructor makes a lot of sense and makes things more self-documented. If everything were a dataclass then this situation would not be explicitly stated in the constructors.
Once the need to tart up the class methods or do any inheritance crops up, make a "proper" class.
None of that has an technical basis, but there is value in being methodical with the coding choices.
```
dataclass NormalizedPoint(x: float, y: float):
_norm = math.sqrt(x\*2 + y\*2)
x /= _norm
y /= _norm
def __add__(other: NormalizedPoint):
return NormalizedPoint(x + other.x, y + other.y)
```In other words, I would really like some construct which sticks as close as possible to a normal function, but that also returns a (typed) object with everything in its scope.
That would be weird in classes that override the constructor (__new__), which is what is actually called (not __init__, except contingent on the result of the constructor) when the class is used as a callable.
Not to mention the need to define methods in the class body, and be able to distinguish them from local functions in the constructor/initializer, which is a pretty good argument against the class body being the constructor or initializer.
It's a pretty significant package, if you're proposing this kind of thing, I assume there's an answer to this...
What's the difference between data and class? Because of what it keeps? So why not have "methods" for classes that only have methods or "notdecided" if you don't yet know what sort of class you are going to end up with.
I don't follow.
It trades away the flexibility of processing the incoming data in __init__ to make defining the class simpler.
I agree that the name is confusing, I think maybe @autoinit would have been better.
What would be the consequences of non-data class that only contains data to treat is as a data class? Likely none.
The consequences are exposing state that you don't necessarily want to expose, missed opportunities for optimization (lose fine-grained memory/performance tradeoff ability), imply something is serializable when it isn't, collisions with metaclasses/inheritance weirdness.
Then, I agree Python is way too big a language now, but I'd rather start removing things like the walrus and operator overloading.
Walrus is fine. It's easy enough to google "python colon equals" and figure it out.
I love dataclasses, but there needs to be a compelling reason to break everyone's code and the reason can't just be "I don't want one import and one decorator".
In a way it feels like data classes are a strange hybrid.
Similarly I see all kinds of type theoretical issues with type checking popping up.
I'm going to go try it in a minute, but I'm specifically thinking of setting a mutable variable to use as a way to cache the results of a long running process across objects. Or even just any variable to use with class methods. I'll update soon
edit:
I tried it out and it looks like it works just fine
@dataclass
class MyCoolDataClass:
arg: any
_things = {'last_updated':0, 'data':[]}
_things_timeout = 300
def _update_things(self):
"""some long process"""
time.sleep(10)
self._things['last_updated'] = time.time()
self._things['data'] = ['some things']
@property
def things(self):
seconds_since = time.time() - self._things.get('last_updated', 0)
if seconds_since >= self._things_timeout:
self._update_things()
return self._things.get('data',[])
which then works as expected, sharing the cache between objects. start_time = time.time()
for i in range(10):
obj = MyCoolDataClass(i)
print(obj.things, int(time.time() - start_time))
['some things'] 10
['some things'] 10
...
So yeah, I think I might be on board with dataclasses by default. As long as we can still add __init__ for when we want to do something extra with the variableedit2:
If we switch to dataclasses things like this would need to raise a syntax error:
@dataclass
class MyOtherDataClass:
arg: any
def __init__(self, special_arg:str):
self.special_arg = special_arg.lower().strip()
As it stands now defining __init__ completely removes any definitions set in the class.I think the killer feature would be if we added django forms style validate methods for validation and normalization. imagine if we could do this:
class Drink:
temp: str
container: str
def _validate_temp(self, temp):
temp = temp.strip().lower()
if temp not in ['hot', 'cold', 'room']:
raise ValidationError(f'"{temp}" is not a valid temperature')
return temp
def _validate_container(self, container):
container = temp.strip().lower()
if container not in ['mug', 'glass' 'sippy cup']:
raise ValidationError(f'"{container} is not a valid container")
return container
def _validate(self, kwargs):
if kwargs['temp'] == 'cold' and kwargs['container'] == 'mug':
raise ValidationError("Don't dirty a mug for a cold drink!")
Sure, it goes against explicit is better than implicit, but that could be fixed with decorators. It would also be nice to have a concise way to say what values are valid, and a handful of standard normalization processes, but I think you get the idea.Does Javascript still have a use for functions without async/await?
This is just a repackaging of an age-old argument, imo.
When they invented the screw, it did not preclude the nail. /shrug
In Python, they invent a weird, slow concept and discuss it for a decade, probably while writing 10 PEPs in the process. Through conferences, Stackoverflow, etc. people are slowly convinced that the concept is natural.
I’m always astonished by ignorant people feigning knowledge of their areas of ignorance.
In haskell, structs are defined with the keyword data. Maybe that is where the inspiration comes from.
https://lotz84.github.io/haskellbyexample/ex/structs
In any case, it seems each language chooses equally confusing keywords. Syntax is not a big deal IMO if the concept is clear.
In Haskell, Rust and Python you can also just have tuples, if you want to. In Haskell you can use `newtype` or `type` to name your tuples and use them as records.
Of course, as you say, you can also use the `data` keyword in Haskell. Or you can use church encoding to use functions as records..
Lots of (confusing) possibilities.
> "dataclasses" have been called struct or record in other languages
For structs you have to remember that in python everything is reference passed, so some of the differences between structs and classes that you see in other languages do not apply.
But I suspect the real practical reason is that python already has structs, which are used for interoperability with C structs. Or any structured binary format really.