Skip to main content

Object-Oriented Programming in Python

A class defines a new type. Each instance can hold state in attributes and expose behavior through methods. Classes are useful when data and the operations that preserve its rules belong together; a function or simple collection may be clearer for smaller tasks.

Defining a class​

__init__ initializes a new instance. Instance methods receive that instance as their first argument, conventionally named self.

class Measurement:
unit = "°C"

def __init__(self, value, sensor):
self.value = value
self.sensor = sensor

def describe(self):
return f"{self.sensor}: {self.value}{self.unit}"


reading = Measurement(21.5, "lab")
print(reading.describe()) # lab: 21.5°C

unit is a class attribute shared through the class unless an instance overrides it. value and sensor are instance attributes.

Protecting invariants​

Python does not enforce private fields in the same way as some languages. A leading underscore communicates that an attribute is non-public. Properties can expose validation while keeping normal attribute syntax.

class Percentage:
def __init__(self, value):
self.value = value

@property
def value(self):
return self._value

@value.setter
def value(self, new_value):
if not 0 <= new_value <= 100:
raise ValueError("percentage must be between 0 and 100")
self._value = new_value


completion = Percentage(75)
completion.value = 80
print(completion.value) # 80

Composition before inheritance​

Composition gives one object another object to collaborate with and often keeps responsibilities clearer than a deep inheritance tree.

class MeanReducer:
def reduce(self, values):
if not values:
raise ValueError("values must not be empty")
return sum(values) / len(values)


class Report:
def __init__(self, reducer):
self.reducer = reducer

def summarize(self, values):
return {"count": len(values), "result": self.reducer.reduce(values)}


report = Report(MeanReducer())
print(report.summarize([2, 4, 6]))

Inheritance and polymorphism​

Inheritance is appropriate when a subtype can safely satisfy the parent type's contract. Python's polymorphism is often structural: code can accept any object that provides the required behavior, without requiring a shared base class.

Prefer small public interfaces, avoid mutable class attributes for per-instance state, and test observable behavior rather than internal attributes.