Skip to content

Classes & Objects

A class is a blueprint. It describes what an object is and what it can do. An instance is a concrete object created from that blueprint, each one carries its own data but shares the same behaviour.

class Dog:
# Class attribute — shared by ALL instances
species = "Canis familiaris"
def __init__(self, name, age):
# Instance attributes — unique to EACH object
self.name = name
self.age = age
def bark(self):
return f"{self.name} says Woof!"
def __str__(self): # print(dog) → human-readable
return f"Dog(name={self.name}, age={self.age})"
def __repr__(self): # repr(dog) → developer-facing
return f"Dog('{self.name}', {self.age})"

species lives on the class — one value, shared by every dog you create. name and age live on the instance — each dog gets its own copy.

dog1 = Dog("Rex", 3)
dog2 = Dog("Buddy", 5)
print(dog1.species) # Canis familiaris ← same for all
print(dog2.species) # Canis familiaris ← same for all
print(dog1.name) # Rex ← unique to dog1
print(dog2.name) # Buddy ← unique to dog2

Visually:

Dog (class)
│ species = "Canis familiaris" ← shared
├── dog1 (instance)
│ name = "Rex", age = 3 ← unique
└── dog2 (instance)
name = "Buddy", age = 5 ← unique

Every method receives self as the first argument. It is how the object refers to itself — how bark() knows which dog is barking.

print(dog1.bark()) # Rex says Woof!
print(dog2.bark()) # Buddy says Woof!

Python rewrites dog1.bark() as Dog.bark(dog1) under the hood — self is just dog1 passed in automatically.

Both control how the object is displayed, but for different audiences:

MethodTriggered byFor
__str__print(dog), str(dog)End users — readable
__repr__REPL, debugger, repr(dog)Developers — unambiguous
print(dog1) # Dog(name=Rex, age=3) ← __str__
repr(dog1) # Dog('Rex', 3) ← __repr__

If you only define one, define __repr__, Python falls back to it when __str__ is missing.

The key idea: the class is the mold, instances are the objects cast from it. The mold stays the same — each cast comes out with its own data.

__repr__ is a special Python method that defines the string representation of an object — what you see when you print or inspect it.

Without __repr__
rectangles = [Rectangle(4, 5), Rectangle(10, 2), Rectangle(3, 3)]
print(rectangles)
# Without `__repr__` you get:
# Useless — just the memory address.
# Three objects. No idea what is in them.
[<Rectangle object at 0x10f3a2b50>, <Rectangle object at 0x10f3a2c60>, <Rectangle object at 0x10f3a2d70>]
# With `__repr__`:
class Rectangle:
def __init__(self, width: float, height: float) -> None:
self.width = width
self.height = height
def __repr__(self) -> str:
return f"Rectangle(width={self.width}, height={self.height})"
print(rectangles)
[Rectangle(width=4, height=5), Rectangle(width=10, height=2), Rectangle(width=3, height=3)]

__repr__ should ideally return a string that looks like valid Python code you could paste back into the interpreter to recreate the object.

Let’s use REPL (Read, Evaluate, Print, Loop) to interact with the Python Interpreter and visually see the __repr__ in action:

__repr__ in action
# Type python or python3 to start REPL
$ python
# Interact with the Python interpreter
>>> r = Rectangle(width=4, height=5)
>>> r
Rectangle(width=4, height=5)
>>> eval(repr(r)) # recreates the object
Rectangle(width=4, height=5)
PurposeCalled by
__repr__Unambiguous, developer-facingrepr(), REPL, debugger
__str__Human-readable, user-facingprint(), str()

If you only define __repr__print() will fall back to it. If you define both, print() uses __str__ and the REPL uses __repr__.

For most classes defining just __repr__ is enough.

A summary of the core concepts covered in this chapter.

ConceptSyntaxPurpose
Define a classclass Dog:Create a blueprint
Create an instanceDog("Rex", 3)Concrete object from the blueprint
Class attributespecies = "Canis familiaris"Shared by all instances
Instance attributeself.name = nameUnique to each instance
Instance methoddef bark(self):Behaviour tied to an instance
MethodTriggered byFor
__str__print(obj), str(obj)End users — readable
__repr__REPL, debugger, repr(obj)Developers — unambiguous
ConceptMeaning
selfReference to the current instance
dog1.bark()Python rewrites as Dog.bark(dog1)
Always first argumentEvery instance method receives self automatically
SituationUse
Primarily storing data@dataclass
Need validation, properties, complex logicplain class
Immutable data@dataclass(frozen=True)
Memory efficiency@dataclass(slots=True)
Only one representation neededDefine __repr__print() falls back to it

Create a Rectangle class that:

  • Takes width and height in __init__
  • Has an area() method
  • Has a perimeter() method
  • Has a __str__ that returns "Rectangle(width=W, height=H)"
  • Has a is_square() method that returns True if width equals height
Expected output
r = Rectangle(4, 6)
print(r) # Rectangle(width=4, height=6)
print(r.area()) # 24
print(r.perimeter()) # 20
print(r.is_square()) # False
s = Rectangle(5, 5)
print(s.is_square()) # True
Solution
exercise_01.py
class Rectangle:
# Constructor/Initializer
def __init__(self,width,height):
self.width = width
self.height = height
def area(self):
return self.height * self.width
def perimeter(self):
return (self.height + self.width) * 2
def __str__(self):
return f"Rectangle(width={self.width}, height={self.height})"
def is_square(self):
return self.width == self.height
test_exercise_01.py
from exercises.ch01_oop.classes.exercise_01 import Rectangle
def test_rectangle_happy_path():
r = Rectangle(4, 6)
print(r)
assert r.area() == 24
assert r.perimeter() == 20
assert r.is_square() is False
def test_rectangle_squared():
r = Rectangle(5, 5)
assert r.is_square() is True