Classes & Objects

A class is a blueprint for objects that bundle data (attributes) with the functions that work on it (methods). Everything in Python is already an object — strings, lists, even functions — and classes let you create your own types that fit your problem.

Defining a class

PY class.py
class Dog:
    species = "Canis familiaris"

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def describe(self):
        return f"{self.name} is {self.age} years old"

    def birthday(self):
        self.age += 1

rex = Dog("Rex", 3)
fido = Dog("Fido", 7)

print(rex.describe())
rex.birthday()
print(rex.age, fido.age)
print(rex.species, fido.species)
Output
Rex is 3 years old
4 7
Canis familiaris Canis familiaris
  • class Dog: defines the type. Class names use PascalCase.
  • __init__ is the initialiser, run automatically when you create an object with Dog(...). It sets up the object's attributes.
  • self is the object the method was called on. Python passes it automatically: rex.describe() calls Dog.describe(rex).
  • self.name = name creates an instance attribute — each object has its own.
  • species is a class attribute, shared by every instance.

Objects are independent

PY independent.py
class Counter:
    def __init__(self):
        self.count = 0

    def increment(self, by=1):
        self.count += by
        return self

a = Counter()
b = Counter()
a.increment().increment(5)
print(a.count, b.count)
print(isinstance(a, Counter), type(a).__name__)
Output
6 0
True Counter

Returning self from a method allows calls to be chained.

Class attribute pitfall

A mutable class attribute is shared by all instances — usually not what you want:

PY shared.py
class BadCart:
    items = []

    def add(self, item):
        self.items.append(item)

class Cart:
    def __init__(self):
        self.items = []

    def add(self, item):
        self.items.append(item)

a, b = BadCart(), BadCart()
a.add("apple")
print(b.items)

c, d = Cart(), Cart()
c.add("apple")
print(d.items)
Output
['apple']
[]

Create per-object data in __init__. Use class attributes only for constants and genuinely shared values.

Readable printing: __repr__ and __str__

PY repr.py
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"

    def __str__(self):
        return f"({self.x}, {self.y})"

p = Point(3, 4)
print(p)
print(repr(p))
print([p, Point(0, 0)])
Output
(3, 4)
Point(x=3, y=4)
[Point(x=3, y=4), Point(x=0, y=0)]

Without these methods, printing shows something like <__main__.Point object at 0x…>. __repr__ is for developers (used in lists, the shell and debuggers); __str__ is for users. Lesson 38 covers more of these "special methods".

Attributes are dynamic

PY dynamic.py
class Box:
    pass

b = Box()
b.width = 10
b.label = "books"
print(vars(b))
print(getattr(b, "height", "no height"))
setattr(b, "height", 5)
print(hasattr(b, "height"))
Output
{'width': 10, 'label': 'books'}
no height
True

You can add attributes to most objects at any time, and getattr/setattr/hasattr access them by name. It's flexible, but defining all attributes in __init__ keeps classes predictable.

Private by convention

Python has no truly private attributes. A leading underscore signals "internal, don't touch from outside":

PY private.py
class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self._balance += amount

    def balance(self):
        return self._balance

acc = Account("Ana", 100)
acc.deposit(50)
print(acc.owner, acc.balance())
Output
Ana 150

A double underscore (self.__balance) triggers name mangling, making accidental access from subclasses harder — but it's still not real privacy. Python relies on convention: "we're all consenting adults".

When to write a class

Classes are worth it when data and the operations on it belong together and you'll create several instances — a BankAccount, a Player, an APIClient. For a bundle of data with no behaviour, a dataclass or dictionary is simpler; for a single function, just write the function.

Try it yourself

Try it yourself
Console output