Data classes
Imagine you draw the same parts on every new blueprint: how to fill in a card, how to label it, how to compare two. Only the names of the fields change. It is more convenient to list the fields and let a helper draw in the parts that never change.
That is exactly what a class that mostly holds data looks like. A Point class with __init__, __repr__ and __eq__ takes about ten lines, and x and y repeat in all three methods. Add a third coordinate, and all three need editing. For such classes, the standard library writes these methods:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int = 0
p = Point(1, 2)
print(p) # Point(x=1, y=2)
print(p == Point(1, 2), Point(5)) # True Point(x=5, y=0)Importing one name
You have already loaded whole modules. The line import math loads the module math, and its names are written with a dot: math.isclose.
The line from dataclasses import dataclass works differently. It takes the single name dataclass from the dataclasses module, and after that it is written without the module name.
A decorator for a class
dataclass is a decorator, except that it takes a class rather than a function. @dataclass over a class works as it does over a def: it equals the assignment Point = dataclass(Point) right after the class body.
The decorator adds methods to the class and returns the same class. A class it is applied to is called a data class.
Fields and annotations
The notation x: int is an annotation: a name, a colon and a type. In a data class, each annotated name in the class body becomes a field. The decorator reads the fields in order and writes three methods:
- an
__init__with the same parameters; - a
__repr__of the formPoint(x=1, y=2); - an
__eq__that compares the fields in order.
Let's look at what else the example shows:
- A value after
=becomes a default value:Point(5)creates a point whoseyis0. - Python does not check the annotation:
Point("a")creates a point too. The type in it is written for the reader. - The comparison
==givesTrueonly for instances of the same class:Point(1, 2) == (1, 2)isFalse.
This is where the helper analogy stops working. The decorator draws nothing as you watch: once, when the class is created, it adds ordinary methods to it. From then on, the class works as if you had written them yourself.
A data class is an ordinary class
The data class's __init__ stores each field in an instance attribute. So each point has its own x and y, even with a value in the class body. After p.x = 10, the point has a new coordinate. Methods are added to a data class the same way as to any class:
@dataclass
class Item:
name: str
price: float
qty: int = 1
def total(self):
return self.price * self.qty
cart = [Item("pen", 1.5, 4), Item("book", 12.0)]
print(sum(item.total() for item in cart)) # 18.0
print(cart[1]) # Item(name='book', price=12.0, qty=1)__repr__ shows the field values through their own __repr__, which is why the string 'book' is in quotes.
Common mistake
A field without a default value is written after a field with one:
@dataclass
class Item:
name: str
qty: int = 1
price: float # ✗TypeError: non-default argument 'price' follows default argumentIn __init__, a parameter without a default value cannot follow a parameter with one, as in an ordinary function. The error comes when the class is created, before the first instance. Move the fields with default values to the end.
Key points
@dataclasswrites__init__,__repr__and__eq__from the class's fields.- A field is an annotated name in the class body; a value after
=is its default value. - Python does not check annotations, and fields without a default value come first.
from dataclasses import dataclasstakes one name from a module, written without the module name.
Subscribe to take the course
The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.