A class is a template; an object is one thing made from it. In Python, object-oriented programming is optional — plenty of excellent Python is plain functions — but classes earn their place when data and the operations on it belong together. This guide covers classes, inheritance, the special methods that make your objects behave like built-in ones, and when not to bother.
A first class#
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def greet(self):
return f"Hello, my name is {self.name}"
def have_birthday(self):
self.age += 1
ana = Person("Ana", 34)
print(ana.greet()) # Hello, my name is Ana
ana.have_birthday()
print(ana.age) # 35
__init__ runs when you create an instance and sets up its data. self is the instance itself, passed automatically — you write it in the definition but not in the call.
Instance and class attributes#
class Dog:
species = "Canis familiaris" # shared by every dog
def __init__(self, name):
self.name = name # unique to this dog
a = Dog("Rex")
b = Dog("Bella")
print(a.species, b.species) # both the same
print(a.name, b.name) # different
Dunder methods#
These are what make your objects work with Python’s built-in syntax:
class Money:
def __init__(self, pence):
self.pence = pence
def __repr__(self):
return f"Money({self.pence})" # for developers
def __str__(self):
return f"£{self.pence / 100:.2f}" # for users
def __eq__(self, other):
return isinstance(other, Money) and self.pence == other.pence
def __hash__(self):
return hash(self.pence) # needed if __eq__ is defined
def __add__(self, other):
return Money(self.pence + other.pence)
def __lt__(self, other):
return self.pence < other.pence
a = Money(1250)
b = Money(399)
print(a) # £12.50
print(a + b) # Money(1649)
print(sorted([a, b])) # uses __lt__
print(a == Money(1250)) # True
Two rules worth remembering. Define __repr__ on every class — it is what appears in tracebacks and debuggers, and the default is useless. And if you define __eq__, define __hash__ too, or your objects cannot go in a set or be used as dictionary keys.
Other useful ones:
def __len__(self): ... # len(obj)
def __getitem__(self, key): ... # obj[key]
def __contains__(self, x): ... # x in obj
def __iter__(self): ... # for x in obj
def __enter__(self) / __exit__(self, *args): ... # with obj:
Inheritance#
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError("Subclasses must implement speak()")
def describe(self):
return f"{self.name} says {self.speak()}"
class Dog(Animal):
def speak(self):
return "Woof"
class Cat(Animal):
def __init__(self, name, indoor=True):
super().__init__(name) # run the parent's setup first
self.indoor = indoor
def speak(self):
return "Meow"
for animal in [Dog("Rex"), Cat("Bella")]:
print(animal.describe())
The loop is polymorphism: the same call produces different behaviour depending on the actual class. Note that describe() was written once and works for both.
Composition is usually better#
Inheritance says “is a”. Composition says “has a”, and it is the right answer more often than beginners expect:
class Engine:
def start(self):
return "running"
class Car:
def __init__(self):
self.engine = Engine() # a car HAS an engine
def start(self):
return self.engine.start()
Deep inheritance hierarchies become hard to follow, because to understand one class you have to read four. A shallow hierarchy with composition inside is easier to change.
The underscore conventions#
class Account:
def __init__(self, balance):
self.balance = balance # public
self._rate = 0.02 # internal, by convention
self.__secret = "hidden" # name-mangled
def _recalculate(self): # internal method
pass
Python has no real private members. A single underscore is a convention meaning “internal, do not rely on this”. A double underscore triggers name mangling, which prevents accidental collisions in subclasses but is not security. One underscore is what you want almost always.
Properties#
class Temperature:
def __init__(self, celsius=0):
self.celsius = celsius # goes through the setter
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
t = Temperature(20)
print(t.fahrenheit) # 68.0 - looks like an attribute
t.celsius = -300 # ValueError
Properties let you add validation or computation later without changing any code that uses the class. That is why Python does not need getters and setters everywhere from the start — start with a plain attribute and convert it if you ever need to.
Dataclasses#
from dataclasses import dataclass, field
@dataclass
class Point:
x: float
y: float
tags: list = field(default_factory=list)
def distance_from_origin(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
p = Point(3, 4)
print(p) # Point(x=3, y=4, tags=[])
print(p == Point(3, 4)) # True
print(p.distance_from_origin()) # 5.0
The decorator generates __init__, __repr__ and __eq__ for you. Use field(default_factory=list) for mutable defaults — a bare [] raises an error, which is the language protecting you from the shared-list bug above.
When not to use a class#
# Not a class
class Calculator:
def add(self, a, b):
return a + b
# Just a function
def add(a, b):
return a + b
A class with no state and one method is a function wearing a costume. Reach for a class when there is data that several operations share, when you need several independent instances, or when different types should respond to the same call differently. Otherwise a function is clearer.
Questions people ask#
Why does every method need self?
Because Python passes the instance explicitly rather than making it implicit. It is more verbose than other languages and considerably less magical — self.x always clearly means “this instance’s x”.
What is the difference between __str__ and __repr__?
__str__ is for people using your program; __repr__ is for developers debugging it. If you only write one, write __repr__ — Python falls back to it when __str__ is absent.
Should I use dataclasses everywhere?
For classes that mainly hold data, yes — they remove a lot of boilerplate. For classes with substantial behaviour and little state, a plain class is fine.
Do I need to inherit from object?
No. In Python 3 every class does automatically. Writing class Foo(object): is a Python 2 habit.
Where to go next#
- What is object-oriented programming? — the ideas without the syntax.
- Understanding __new__ — the step before
__init__. - Python functions explained — often the better answer.