Skip to content

Classes and Objects

Python has two common shapes for grouping data and behavior together. For typed data, reach for a dataclass first. For classes with more substantial logic or full control over construction, write a regular class.

Dataclasses

A @dataclass is a regular class with __init__, __repr__, and equality generated automatically from the typed field declarations:

Python
from dataclasses import dataclass


@dataclass
class Project:
    name: str
    slug: str
    archived: bool = False


payments = Project(name="Payments API", slug="payments-api")
print(payments)
# Project(name='Payments API', slug='payments-api', archived=False)

print(payments.archived)
# False

The decorator reads the class body, sees three typed attributes, and fills in __init__(self, name, slug, archived=False) for you. The generated __repr__ is the readable string above, and the generated __eq__ compares fields, so Project("a", "a") == Project("a", "a") is True.

You can still add methods. The decorator only generates the boilerplate; the rest of the class body is normal Python:

Python
@dataclass
class Project:
    name: str
    slug: str
    archived: bool = False

    def archive(self) -> None:
        self.archived = True


payments = Project(name="Payments API", slug="payments-api")
payments.archive()
print(payments.archived)
# True

For typed data with light behavior, dataclasses are the modern default. Pydantic and SQLModel extend the same pattern with runtime validation and database mapping.

Regular Classes

When a class needs more substantial logic in __init__, multiple constructors, or behavior that doesn't fit a "bag of fields" shape, write a regular class. The mechanics that dataclasses generate are the same ones you'd write by hand:

Python
class Project:
    def __init__(self, name, slug, archived=False):
        self.name = name
        self.slug = slug
        self.archived = archived

    def archive(self):
        self.archived = True

__init__ runs when you create the instance. The first parameter on every method is self, which refers to the instance the method was called on. payments.archive() is shorthand for Project.archive(payments) — Python passes self for you when you use the dot syntax.

Instance vs Class Attributes

Attributes assigned inside __init__ belong to the specific instance. Attributes assigned at the class body level are shared across every instance:

Python
class Project:
    default_separator = "-"  # class attribute, shared

    def __init__(self, name):
        self.name = name  # instance attribute, per-object

Project.default_separator is the same value for every instance.

self.name is different for each one.

Tip

Use class attributes for things that are genuinely the same across all instances (defaults, constants, configuration) and instance attributes for everything that varies per object.