python · level 5

Classes

Objects, dunders, dataclasses, properties.

125 XP

Python Classes

A class is a blueprint for objects. Define the shape once, instantiate as many as you need.

The basics

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def display_name(self):
        return self.name.title()

alice = User("alice", "alice@x.com")
alice.display_name()                 # "Alice"

__init__ is the constructor. self is the convention for the instance reference (it's a parameter, not a keyword — you could name it anything, but don't).

Class attributes vs instance attributes

class Counter:
    total_created = 0          # class attribute — shared

    def __init__(self):
        self.value = 0         # instance attribute — per object
        Counter.total_created += 1

Reading Counter.total_created on the class works. Reading it on an instance also works (Python checks the instance first, then the class). Writing to it on an instance creates a NEW instance attribute that shadows the class one — a subtle gotcha.

dunder methods

Methods named __like_this__ ("dunder" = double-underscore) hook into Python syntax:

class Money:
    def __init__(self, cents):
        self.cents = cents

    def __repr__(self):
        return f"Money({self.cents})"
    def __add__(self, other):
        return Money(self.cents + other.cents)
    def __eq__(self, other):
        return isinstance(other, Money) and self.cents == other.cents
    def __hash__(self):
        return hash(self.cents)

a = Money(100) + Money(200)            # __add__
print(a)                                # Money(300) — __repr__
{Money(100): "x"}                       # __hash__ + __eq__

The big ones: __init__, __repr__, __eq__, __hash__, __lt__, __add__, __len__, __iter__, __contains__, __call__.

Inheritance

class Animal:
    def speak(self):
        return "..."

class Dog(Animal):
    def speak(self):
        return "Woof"

class Puppy(Dog):
    def speak(self):
        return super().speak() + "!"

Puppy().speak()                        # "Woof!"

super() calls the parent's version. Multiple inheritance works too (class C(A, B):); Python uses C3 linearisation for method resolution order. For most apps, prefer composition.

dataclasses

@dataclass (3.7+) writes __init__, __repr__, and __eq__ for you:

from dataclasses import dataclass

@dataclass
class User:
    name: str
    email: str
    age: int = 0

Equivalent to writing __init__, __repr__, __eq__ by hand. There's also frozen=True to make instances immutable, and kw_only=True to force keyword-only args.

Properties

A @property makes a method look like an attribute:

class Circle:
    def __init__(self, r):
        self.r = r

    @property
    def area(self):
        return 3.14159 * self.r ** 2

c = Circle(5)
c.area                                # 78.539... (no parens)

Pair with a setter for write access, or omit it for read-only.

Class methods + static methods

class User:
    @classmethod
    def from_row(cls, row):           # alternate constructor
        return cls(name=row[0], email=row[1])

    @staticmethod
    def is_email(s):                  # not tied to an instance OR class
        return "@" in s

Naming conventions

  • name — public attribute.
  • _name — private by convention (Python doesn't enforce it).
  • __name — name-mangled (_ClassName__name); use sparingly.
  • __name__ — dunder; reserved by Python.

When NOT to use a class

If you have a few related fields and one or two functions over them, a @dataclass is fine — but a plain dict or namedtuple is often enough. Python isn't Java; you don't need a class for everything.