Python Classes: When a Dictionary Is No Longer Enough
When to move from dictionaries to classes, how to write a class with methods, and when a dataclass is enough. After this, you can group data with the rules that belong to it.
Course content
Sign in to save your progress.
-
Set Up Projects That Don't Break Each Other
-
Python Virtual Environments and pip: One Project, One Environment
-
Pinning Python Dependencies With requirements.txt
-
-
-
Organize Code Other People Can Read
-
Python Modules and Packages: Splitting One Big File
-
Python Imports: Why ModuleNotFoundError Keeps Happening
-
Python Classes: When a Dictionary Is No Longer Enough
-
-
-
Work With Real Files
-
Python File Handling: Open, Read, Write, and Always Use with
-
Python pathlib: Stop Building File Paths by Hand
-
Reading, Cleaning and Writing CSV Data in Python
-
-
-
Shorter Code That Still Says What It Does
-
Python Comprehensions: Lists, Sets and Dictionaries
-
When a Comprehension Should Be a Plain Loop
-
-
-
Prove It Works Before Someone Else Finds Out
-
Python Testing Basics With pytest
-
Debugging Python With print, logging and pdb
-
-
- Final Check: Python Intermediate
A dictionary is fine for holding data. It stops being enough when the data comes with rules: a balance that can't go negative, a total that must be calculated the same way everywhere, a status that only changes in certain steps. That is when you need a class. Before that, you probably don't.
Skip ahead: If you already write classes with __init__, methods, __repr__ and @dataclass, take the section quiz.
The problem with dictionaries and rules
Here is an order as a dictionary, plus a function that works on it:
order = {"customer": "Ana", "items": [], "status": "open"}
def add_item(order, name, price, qty):
order["items"].append({"name": name, "price": price, "qty": qty})
add_item(order, "Keyboard", 1200, 1)
order["itmes"] = [] # typo: creates a new key, no error
order["status"] = "shipd" # typo: invalid status, no error
print(order)
{'customer': 'Ana', 'items': [{'name': 'Keyboard', 'price': 1200, 'qty': 1}], 'status': 'shipd', 'itmes': []}
Two typos, zero errors. The dictionary accepts anything. And every function that touches an order has to know the keys and the rules by heart. If you have ten such functions spread across three files, the rules are spread too.
The same thing as a class
class Order:
STATUSES = ("open", "paid", "shipped")
def __init__(self, customer):
self.customer = customer
self.items = []
self.status = "open"
def add_item(self, name, price, qty=1):
if self.status != "open":
raise ValueError(f"Cannot add items to a {self.status} order")
if qty <= 0:
raise ValueError("Quantity must be at least 1")
self.items.append((name, price, qty))
def total(self):
return sum(price * qty for _name, price, qty in self.items)
def mark_paid(self):
self.status = "paid"
order = Order("Ana")
order.add_item("Keyboard", 1200)
order.add_item("Mouse", 450, qty=2)
print(order.total())
order.mark_paid()
order.add_item("Cable", 150)
2100
Traceback (most recent call last):
...
ValueError: Cannot add items to a paid order
The data and the rules that belong to it now live in one place. You can't add items to a paid order anywhere in the program, because the only way to add items is the method that checks.
The pieces:
class Order:defines the blueprint.Order("Ana")creates one actual order, called an instance.__init__runs when an instance is created. It sets the starting values.selfis the instance the method is working on.order.total()is the same asOrder.total(order). Python passes it for you.- Methods are functions that belong to the class. They read and change
self.
Make it print something useful
class Order:
def __init__(self, customer):
self.customer = customer
self.items = []
print(Order("Ana"))
<__main__.Order object at 0x...>
That memory address is useless when you are debugging. Add __repr__:
class Order:
def __init__(self, customer):
self.customer = customer
self.items = []
def __repr__(self):
return f"Order(customer={self.customer!r}, items={len(self.items)})"
print(Order("Ana"))
Order(customer='Ana', items=0)
When a dataclass is enough
Many classes are mostly data with a few small methods. Writing __init__ and __repr__ by hand for those is busywork. @dataclass writes them for you:
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
stock: int = 0
def in_stock(self):
return self.stock > 0
keyboard = Product("Keyboard", 1200, stock=5)
print(keyboard)
print(keyboard.in_stock())
print(keyboard == Product("Keyboard", 1200, 5))
Product(name='Keyboard', price=1200, stock=5)
True
True
You also get == that compares the values instead of checking if it is the same object. The type annotations (str, float) are required by the dataclass syntax, but Python doesn't enforce them at runtime. Product("Keyboard", "cheap") would still be accepted. Checking types is covered in Python Advanced.
⚠ Warning: Never use a list or dictionary as a default value in __init__, like def __init__(self, items=[]):. All instances end up sharing the same list, so adding an item to one order adds it to all of them. Use items=None and create the list inside. In a dataclass, use field(default_factory=list). Python refuses the plain = [] there for exactly this reason.
When not to write a class
Classes are not automatically better. Some honest cases where they aren't:
- A class with one method and no data. That is a function wearing a costume. Write the function.
- Data you only pass along. If you read JSON and write it somewhere else without rules in between, a dictionary is fine.
- Deep inheritance.
class Animal→class Dog(Animal)→class SmallDog(Dog)looks neat in examples and hurts in real code. Most business code needs little or no inheritance.
Common mistakes
- Forgetting
self. Writingdef total():gives "takes 0 positional arguments but 1 was given" when you call it. - Using a local variable instead of
self.items = []inside__init__disappears when it finishes.self.items = []stays with the instance. - Changing attributes from outside.
order.status = "shipd"still works on a class. Python doesn't stop it. Give people a method likemark_paid()and use it consistently, so the rules live in one place.
💡 Tip: A good first class is one you already have as a dictionary plus several functions that take it as their first argument. Those functions are your methods. Move them in and change order["status"] to self.status.
What happens if you skip this step
The rules for your data get copied into every function that uses it. One copy forgets to check the status. Another one spells a key differently. Nothing crashes. The data just slowly becomes wrong, and you find out from a report that doesn't add up.
Look at the dictionary you pass around the most in your code. How many different functions currently decide what a valid version of it looks like?
About
When to move from dictionaries to classes, how to write a class with methods, and when a dataclass is enough. After this, you can group data with the rules that belong to it.
- Duration
- 5 min
- Last update
- 09/22/2026
Comments (0)
Sign in to commentNo comments yet.