20.1 Introduction to Dataclasses
A dataclass is a special kind of Python class designed mainly for storing related data. It automatically creates useful methods such as an initializer, a readable text representation, and object comparison. Dataclasses reduce repeated code, so beginners can focus on the information an object should contain instead of writing the same class methods manually.
Example
from dataclasses import dataclass
@dataclass
class Student:
name: str
age: int
student = Student("Mina", 18)
print(student)
Output
Student(name='Mina', age=18)
Output Explanation: The @dataclass decorator tells Python to generate common class methods automatically. Python creates an initializer that accepts name and age. When the object is printed, the generated representation displays the class name and both stored values.
20.2 Creating Dataclasses
To create a dataclass, import dataclass, place the @dataclass decorator above the class, and describe each field using a name and type annotation. Python uses those fields to build the constructor. You can then create objects by passing values in the same order as the fields appear.
Example
from dataclasses import dataclass
@dataclass
class Book:
title: str
pages: int
price: float
book = Book("Python Basics", 250, 29.99)
print(book.title)
print(book.pages)
print(book.price)
Output
Python Basics
250
29.99
Output Explanation: The Book dataclass contains three fields. The object receives one value for each field. Accessing book.title, book.pages, and book.price prints the values stored inside the object.
20.3 Default Values
A default value is used when the programmer does not provide a value for a field. Defaults make object creation easier when some information commonly stays the same. Fields with default values must usually come after fields without defaults, because Python needs to know which constructor arguments are required and which ones are optional.
Example
from dataclasses import dataclass
@dataclass
class User:
username: str
active: bool = True
user1 = User("ali")
user2 = User("sara", False)
print(user1)
print(user2)
Output
User(username='ali', active=True)
User(username='sara', active=False)
Output Explanation: The first object does not provide an active value, so Python uses the default value True. The second object provides False, which replaces the default for that object.
20.4 Default Factories
A default factory creates a new default object for every dataclass instance. It is especially important for mutable values such as lists, sets, and dictionaries. Using one shared list as a normal default could cause several objects to accidentally change the same data. A factory safely gives each object its own separate collection.
Example
from dataclasses import dataclass, field
@dataclass
class ShoppingList:
owner: str
items: list[str] = field(default_factory=list)
list1 = ShoppingList("Mina")
list2 = ShoppingList("Omid")
list1.items.append("Milk")
print(list1.items)
print(list2.items)
Output
['Milk']
[]
Output Explanation: The default_factory=list creates a different empty list for every object. Adding Milk to list1 does not change list2, so its list remains empty.
20.5 Frozen Dataclasses
A frozen dataclass prevents its fields from being changed after the object is created. This is useful when an object should behave like a fixed record. Frozen objects can make programs safer because important data cannot be accidentally replaced. Attempting to assign a new value to a frozen field raises an error.
Example
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
x: int
y: int
point = Coordinate(4, 7)
print(point.x)
print(point.y)
Output
4
7
Output Explanation: The object is successfully created with x equal to 4 and y equal to 7. Because the dataclass is frozen, these values can be read but should not be reassigned later.
20.6 Ordered Dataclasses
An ordered dataclass can compare objects using operators such as less than, greater than, less than or equal to, and greater than or equal to. Python compares the fields in the order they are declared. Ordered dataclasses are useful when records need to be sorted according to their stored values.
Example
from dataclasses import dataclass
@dataclass(order=True)
class Score:
points: int
player: str
score1 = Score(80, "Ali")
score2 = Score(95, "Sara")
print(score1 < score2)
print(sorted([score2, score1]))
Output
True
[Score(points=80, player='Ali'), Score(points=95, player='Sara')]
Output Explanation: Python first compares the points field because it appears first. Since 80 is less than 95, the first comparison is True. Sorting places the lower score before the higher score.
20.7 Post-Initialization Processing
The __post_init__() method runs immediately after the dataclass initializer finishes. It is useful for calculating a value, validating input, or adjusting data after all fields have been assigned. This lets a dataclass keep automatically generated initialization while still performing additional setup work.
Example
from dataclasses import dataclass
@dataclass
class Rectangle:
width: float
height: float
area: float = 0
def __post_init__(self):
self.area = self.width * self.height
shape = Rectangle(5, 3)
print(shape.area)
Output
15
Output Explanation: Python first stores width as 5 and height as 3. Then __post_init__() multiplies them and stores the result in area. Printing the area displays 15.
20.8 Comparing Dataclass Objects
Dataclasses normally generate an equality method automatically. Two objects are considered equal when they are instances of the same dataclass and all comparable fields contain equal values. This is convenient because you do not need to write a custom __eq__() method for simple data objects.
Example
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
product1 = Product("Keyboard", 40.0)
product2 = Product("Keyboard", 40.0)
product3 = Product("Mouse", 20.0)
print(product1 == product2)
print(product1 == product3)
Output
True
False
Output Explanation: The first two objects contain the same name and price, so Python considers them equal. The third object has different field values, so the second comparison produces False.
20.9 Converting Dataclasses
Python provides helper functions that convert dataclass objects into dictionaries or tuples. The asdict() function creates a dictionary, while astuple() creates a tuple. These conversions are helpful when saving data, preparing information for JSON, sending values to another function, or displaying records in another format.
Example
from dataclasses import dataclass, asdict, astuple
@dataclass
class Person:
name: str
age: int
person = Person("Nima", 25)
print(asdict(person))
print(astuple(person))
Output
{'name': 'Nima', 'age': 25}
('Nima', 25)
Output Explanation: The first function uses field names as dictionary keys and field values as dictionary values. The second function returns only the values in field order, producing a tuple.
20.10 Introduction to Enums
An enum is a class that represents a fixed group of named choices. Instead of using unexplained numbers or strings throughout a program, enums provide clear names such as PENDING, SHIPPED, or DELIVERED. They make code easier to read and reduce mistakes caused by invalid values.
Example
from enum import Enum
class OrderStatus(Enum):
PENDING = "pending"
SHIPPED = "shipped"
DELIVERED = "delivered"
status = OrderStatus.SHIPPED
print(status)
print(status.value)
Output
OrderStatus.SHIPPED
shipped
Output Explanation: Printing the enum member shows its class and member name. Accessing status.value prints the actual value assigned to that member, which is the string shipped.
20.11 Creating Enums
To create an enum, import Enum, inherit from it, and define named members inside the class. Each member receives a value. Program code should normally refer to the member name rather than repeating the raw value. This keeps choices organized in one reliable location.
Example
from enum import Enum
class Direction(Enum):
NORTH = 1
EAST = 2
SOUTH = 3
WEST = 4
direction = Direction.NORTH
print(direction.name)
print(direction.value)
Output
NORTH
1
Output Explanation: The name property returns the member name NORTH. The value property returns the value assigned to that member, which is 1.
20.12 Auto Values
The auto() function automatically assigns values to enum members. It is useful when the exact numeric values are not important and the program mainly needs unique named choices. Auto values reduce typing and prevent accidental duplicate numbering when more members are added later.
Example
from enum import Enum, auto
class Priority(Enum):
LOW = auto()
MEDIUM = auto()
HIGH = auto()
print(Priority.LOW.value)
print(Priority.MEDIUM.value)
print(Priority.HIGH.value)
Output
1
2
3
Output Explanation: Python automatically gives the members increasing integer values. In this example, LOW receives 1, MEDIUM receives 2, and HIGH receives 3.
20.13 Integer Enums
An integer enum uses members that also behave like integers. Python provides IntEnum for this purpose. It is useful when a program needs meaningful enum names but must also compare or pass the values as ordinary integers, such as status codes, levels, or menu selections.
Example
from enum import IntEnum
class AccessLevel(IntEnum):
GUEST = 1
USER = 2
ADMIN = 3
level = AccessLevel.ADMIN
print(level)
print(level == 3)
print(level > AccessLevel.USER)
Output
3
True
True
Output Explanation: Because AccessLevel inherits from IntEnum, its members can behave like integers. The admin level equals 3 and is greater than the user level.
20.14 Flag Enums
A flag enum allows several named options to be combined into one value. It is useful for permissions, settings, or features where more than one choice may be active at the same time. Python uses bitwise operations internally, but beginners can combine flags clearly with the vertical bar operator.
Example
from enum import Flag, auto
class Permission(Flag):
READ = auto()
WRITE = auto()
DELETE = auto()
user_permissions = Permission.READ | Permission.WRITE
print(user_permissions)
print(Permission.READ in user_permissions)
print(Permission.DELETE in user_permissions)
Output
Permission.READ|WRITE
True
False
Output Explanation: The user receives both read and write permissions. The membership checks show that READ is included, while DELETE is not included.
20.15 Named Tuples
A named tuple is a lightweight record that stores values like a tuple but lets you access them by meaningful names. It is useful when you want simple, fixed, read-only data without creating a full class. Named tuples are compact, easy to print, and support both field access and normal tuple indexing.
Example
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
point = Point(10, 20)
print(point.x)
print(point.y)
print(point[0])
Output
10
20
10
Output Explanation: The values can be accessed using the field names x and y. Because the object is also a tuple, index 0 returns the first value, which is 10.
20.16 Slots
Slots are a class feature that limits which instance attributes can exist. Normal objects usually store attributes in a flexible dictionary. A slotted class stores only the names declared in its slots. This can reduce memory usage and prevent accidental creation of misspelled or unexpected attributes.
Example
class Student:
__slots__ = ("name", "grade")
def __init__(self, name, grade):
self.name = name
self.grade = grade
student = Student("Lina", 9)
print(student.name)
print(student.grade)
Output
Lina
9
Output Explanation: The object stores only the declared name and grade attributes. The values assigned in the initializer can be read normally and are printed on separate lines.
20.17 The __slots__ Attribute
The __slots__ attribute lists the instance attributes that a class allows. When it is present, Python usually does not create the normal per-object attribute dictionary. This makes the object structure more controlled. Attempting to add an undeclared attribute normally raises an AttributeError.
Example
class Car:
__slots__ = ("brand", "year")
def __init__(self, brand, year):
self.brand = brand
self.year = year
car = Car("Toyota", 2026)
print(car.brand)
print(car.year)
print(hasattr(car, "__dict__"))
Output
Toyota
2026
False
Output Explanation: The first two lines display the allowed attributes. The last line is False because this simple slotted object does not have the usual instance __dict__.
20.18 Descriptors Introduction
A descriptor is an object that controls how another class attribute is read, assigned, or deleted. Properties, methods, and several Python tools use the descriptor system internally. Descriptors are an advanced feature, but the main idea is simple: one reusable object can manage access rules for attributes in many class instances.
Example
class UpperCase:
def __get__(self, instance, owner):
return instance._name
def __set__(self, instance, value):
instance._name = value.upper()
class Person:
name = UpperCase()
def __init__(self, name):
self.name = name
person = Person("mina")
print(person.name)
Output
MINA
Output Explanation: Assigning mina calls the descriptor's __set__() method. That method converts the text to uppercase before storing it. Reading the attribute calls __get__(), which returns MINA.
20.19 Custom Descriptors
A custom descriptor can validate or transform values whenever an attribute is assigned. It usually defines methods such as __get__(), __set__(), and sometimes __delete__(). Custom descriptors are useful when several classes or fields need the same validation behavior without repeating property code.
Example
class PositiveNumber:
def __set_name__(self, owner, name):
self.storage_name = "_" + name
def __get__(self, instance, owner):
return getattr(instance, self.storage_name)
def __set__(self, instance, value):
if value <= 0:
raise ValueError("Value must be positive")
setattr(instance, self.storage_name, value)
class Product:
price = PositiveNumber()
def __init__(self, price):
self.price = price
product = Product(25)
print(product.price)
Output
25
Output Explanation: The descriptor checks that the price is greater than zero before storing it. Because 25 is valid, it is saved and later returned when product.price is printed.
20.20 Metaclasses Introduction
A metaclass is the class used to create other classes. Most Python classes are created by the built-in metaclass named type. Metaclasses can inspect or modify a class when it is defined. They are advanced and uncommon in beginner projects, but understanding them helps explain how Python classes themselves are objects.
Example
class AddCategory(type):
def __new__(mcls, name, bases, namespace):
namespace["category"] = "Example"
return super().__new__(mcls, name, bases, namespace)
class Item(metaclass=AddCategory):
pass
print(Item.category)
print(type(Item))
Output
Example
<class '__main__.AddCategory'>
Output Explanation: The metaclass adds a category attribute while creating the Item class. The second line shows that Item was created by the custom metaclass AddCategory.
20.21 Practical Class Design
Good class design means choosing the simplest tool that clearly represents the data and behavior. Use a dataclass for data-focused objects, an enum for fixed choices, a named tuple for small read-only records, and a regular class when detailed behavior is required. Clear names and small responsibilities make classes easier to understand and maintain.
Example
from dataclasses import dataclass
from enum import Enum
class Membership(Enum):
BASIC = "Basic"
PREMIUM = "Premium"
@dataclass
class Customer:
name: str
membership: Membership
def description(self):
return f"{self.name}: {self.membership.value}"
customer = Customer("Sara", Membership.PREMIUM)
print(customer.description())
Output
Sara: Premium
Output Explanation: The enum safely represents the allowed membership choices. The dataclass stores the customer's information, and the method combines the name with the readable enum value.
20.22 Chapter Practice Exercises
Practice exercises help you remember when to use dataclasses, enums, named tuples, slots, descriptors, and related class tools. Try each exercise independently before checking an answer. Begin with simple data records and fixed choices, then move toward validation and class customization. Rewriting examples with your own names and values improves understanding.
Example
from dataclasses import dataclass
from enum import Enum
class Size(Enum):
SMALL = "Small"
MEDIUM = "Medium"
LARGE = "Large"
@dataclass
class Shirt:
color: str
size: Size
shirt = Shirt("Blue", Size.MEDIUM)
print(shirt.color)
print(shirt.size.value)
Output
Blue
Medium
Output Explanation: This practice example combines two chapter ideas. The dataclass stores the shirt information, while the enum limits the size to named choices. The output displays the color and the readable enum value.
20.23 Chapter Mini Project
This mini project builds a small task system using a dataclass and an enum. The enum controls the allowed task statuses, while the dataclass stores each task's title, priority, and status. A method produces a readable summary. This design is clearer and safer than using unrelated dictionaries and unverified status strings.
Example
from dataclasses import dataclass
from enum import Enum
class TaskStatus(Enum):
TODO = "To Do"
WORKING = "Working"
DONE = "Done"
@dataclass
class Task:
title: str
priority: int
status: TaskStatus = TaskStatus.TODO
def summary(self):
return (
f"{self.title} | "
f"Priority: {self.priority} | "
f"Status: {self.status.value}"
)
task1 = Task("Study dataclasses", 1)
task2 = Task("Build mini project", 2, TaskStatus.WORKING)
print(task1.summary())
print(task2.summary())
Output
Study dataclasses | Priority: 1 | Status: To Do
Build mini project | Priority: 2 | Status: Working
Output Explanation: The first task uses the default status TODO. The second task explicitly uses WORKING. The summary() method formats the title, priority, and readable status value into one line for each task.