Skip to Content
Python Intermediate: Write Code You Won't Be Embarrassed to Share
Intermediate
0 likes
Courses
Python Intermediate: Write Code You Won't Be Embarrassed to Share

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.

Course Home

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.
  • self is the instance the method is working on. order.total() is the same as Order.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. Writing def 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 like mark_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
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.