Classes & Instances
Classes & Instances
Object-Oriented Programming (OOP) models the world as objects that combine data (attributes) and behaviour (methods). Python supports OOP fully while also allowing other styles.
Defining a Class
class Dog:
species = "Canis familiaris" # class variable — shared by all instances
def __init__(self, name, age): # initialiser
self.name = name # instance variable
self.age = age
def bark(self):
return f"{self.name} says Woof!"__init__is called automatically when you create an instance.selfrefers to the specific instance being created or operated on.- Class variables are shared; instance variables belong to one object.
str and repr
def __str__(self): # human-readable; called by print() and str()
return f"Dog(name={self.name}, age={self.age})"
def __repr__(self): # unambiguous; called in the REPL and by repr()
return f"Dog({self.name!r}, {self.age!r})"@property
@property turns a method into a read-only attribute. Pair it with @<name>.setter for validation:
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self):
import math
return math.pi * self._radius ** 2@classmethod and @staticmethod
| Decorator | First param | When to use |
|---|---|---|
| (none) | self — the instance | Regular methods |
@classmethod | cls — the class itself | Alternative constructors, class-level operations |
@staticmethod | (none) | Utility functions logically grouped with the class |
class Date:
def __init__(self, y, m, d):
self.year, self.month, self.day = y, m, d
@classmethod
def from_string(cls, s): # factory: Date.from_string("2024-01-15")
y, m, d = map(int, s.split("-"))
return cls(y, m, d)
@staticmethod
def is_leap(year): # utility: Date.is_leap(2024)
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)Code Examples
class BankAccount:
bank_name = "PyBank" # class variable
def __init__(self, owner, balance=0.0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self.balance += amount
return self.balance
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
return self.balance
def __str__(self):
return f"{self.bank_name} account | Owner: {self.owner} | Balance: ${self.balance:.2f}"
def __repr__(self):
return f"BankAccount(owner={self.owner!r}, balance={self.balance!r})"
acc = BankAccount("Alice", 1000.0)
acc.deposit(500)
acc.withdraw(200)
print(acc)
print(repr(acc))
print("Bank:", BankAccount.bank_name)__str__ is meant for end users and is called by print(). __repr__ is for developers — it should ideally be a string you could eval() to recreate the object. Class variables like bank_name are shared across all instances.
import math
class Circle:
def __init__(self, radius):
self.radius = radius # calls the setter
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError(f"Radius must be non-negative, got {value}")
self._radius = value
@property
def diameter(self):
return self._radius * 2
@property
def area(self):
return round(math.pi * self._radius ** 2, 4)
@property
def circumference(self):
return round(2 * math.pi * self._radius, 4)
c = Circle(5)
print(f"Radius: {c.radius}")
print(f"Diameter: {c.diameter}")
print(f"Area: {c.area}")
print(f"Circumference: {c.circumference}")
c.radius = 10
print(f"New area: {c.area}")@property converts a method into a computed attribute. The setter validates the input before storing it in the private _radius attribute. Derived properties like area and circumference are always computed fresh from the current radius.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@classmethod
def from_fahrenheit(cls, f):
return cls((f - 32) * 5 / 9)
@classmethod
def from_kelvin(cls, k):
return cls(k - 273.15)
@staticmethod
def is_freezing(celsius):
return celsius <= 0
@property
def fahrenheit(self):
return self.celsius * 9 / 5 + 32
def __str__(self):
return f"{self.celsius:.2f} C / {self.fahrenheit:.2f} F"
t1 = Temperature(100)
t2 = Temperature.from_fahrenheit(212)
t3 = Temperature.from_kelvin(373.15)
print(t1)
print(t2)
print(t3)
print("Is -5C freezing?", Temperature.is_freezing(-5))
print("Is 20C freezing?", Temperature.is_freezing(20))@classmethod receives cls as the first argument, letting it create instances via cls(...) — perfect for alternative constructors. @staticmethod receives no implicit first argument; it's a plain function that logically belongs to the class namespace.
Quick Quiz
1. What is the difference between a class variable and an instance variable?
2. When is __str__ called automatically?
3. What is the primary use case for @classmethod?
Was this lesson helpful?