Skip to Content
Course content

130: Dataclasses: Defaults, Ordering, and Frozen Instances

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a colleague who was building a high-frequency trading pipeline. He had a TradeConfig dataclass that held things like API keys and timeout settings. The bug was subtle: one of the validation functions was "cleaning" the config object by updating a timeout value if it seemed too high. Because the object was mutable, that change propagated everywhere. Suddenly, the entire system was timing out prematurely, and we spent an entire afternoon hunting for the one line of code that was mutating a shared configuration object. If that class had been frozen, the code would have crashed immediately at the point of mutation, telling us exactly where the mistake was.

The Danger of Mutable Defaults

Before we get to freezing instances, we need to talk about defaults. You've probably noticed that giving a field a simple default value like name: str = "Unknown" works perfectly. But the moment you try to use a list or a dictionary, you're walking into a trap. If you write tags: list = [], Python creates that list once when the class is defined, not every time you create a new instance. Every single object you instantiate will share the exact same list in memory.

To fix this, we use field(default_factory=...). This tells Python to call a specific function (like list or dict) every time a new instance is created. Here is how that looks in practice:

from dataclasses import dataclass, field

@dataclass
class UserProfile:
    username: str
    # This ensures every user gets their own unique list of roles
    roles: list[str] = field(default_factory=list)
    bio: str = "No bio provided"

user1 = UserProfile("alice")
user2 = UserProfile("bob")
user1.roles.append("admin")

print(user2.roles) # Output: [] (Correct!)

Locking Down Data with Frozen Instances

As I mentioned in my trading bot story, there are times when you want to guarantee that an object cannot be changed after it is created. By setting frozen=True in the dataclass decorator, Python generates a class that prevents attribute assignment. If you try to change a value, you'll get a FrozenInstanceError.

Beyond just preventing bugs, making a dataclass frozen has a huge technical advantage: it makes the object hashable. This means you can use a frozen dataclass instance as a key in a dictionary or store it in a set, which is impossible with a standard mutable dataclass.

@dataclass(frozen=True)
class Coordinate:
    x: int
    y: int

point = Coordinate(10, 20)
# point.x = 15  # This would raise a FrozenInstanceError

# Because it's frozen, we can use it in a set
locations = {Coordinate(0, 0), Coordinate(10, 20)}

Automatic Ordering and Comparison

By default, dataclasses only implement __eq__, meaning two objects are equal if all their fields match. But what if you need to sort a list of these objects? You could write your own __lt__ (less than) method, but that's a lot of boilerplate. Instead, you can use order=True.

When order=True is set, Python automatically generates the comparison methods (<, >, <=, >=). It does this by comparing the fields as a tuple in the order they are defined in the class. If the first fields are equal, it moves to the second, and so on. This is incredibly powerful for things like priority queues or sorted registries.

@dataclass(order=True)
class Task:
    priority: int
    name: str

# Tasks are sorted primarily by priority, then by name
todo_list = [
    Task(2, "Clean room"),
    Task(1, "Buy groceries"),
    Task(2, "Answer emails"),
]

print(sorted(todo_list)) 
# Output: [Task(priority=1, name='Buy groceries'), Task(priority=2, name='Answer emails'), Task(priority=2, name='Clean room')]



📋 Practical Task

Exercise: Building an Immutable Game Item Registry

You are building a system for an RPG where items have a rarity level, a power score, and a list of magical properties. You need to ensure that once an item is created, its stats cannot be accidentally changed by the game engine, and you need to be able to sort items by their rarity and power.

Requirements:

  • Create a dataclass named GameItem.
  • The class must be frozen to prevent mutations.
  • The class must support ordering.
  • Fields should be:
    • rarity (int): Lower numbers represent higher rarity (e.g., 1 is Legendary, 5 is Common).
    • power (int): The strength of the item.
    • name (str): The name of the item.
    • properties (list): A list of strings describing magical effects. This must use a default_factory to ensure each item has its own list.

Testing your code:

Create three items with different rarities and powers. Put them in a list and use sorted() to verify that they are ordered correctly (primarily by rarity, then by power). Try to change the name of one item to verify that it raises a FrozenInstanceError.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.