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:
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:
@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:
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:
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.