Understand OOP through real industrial equipment — drills, sensors, ore veins, and blasting teams. No abstract animals or shapes. Every concept includes a practical, domain‑driven example. Now with three advanced topics: metaclasses, context managers, and descriptors.
A class defines shared structure; an instance holds its own state. Here MiningDrill has a class attribute category and per‑drill attributes. The self parameter gives access to the concrete object.
class MiningDrill: category = "rotary" def __init__(self, model: str, power_kw: int): self.model = model self.power_kw = power_kw self.is_running = False def start(self) -> str: self.is_running = True return f"{self.model} started — {self.power_kw} kW" drill_a = MiningDrill("X900", 450) drill_b = MiningDrill("Z1100", 620) print(drill_a.start()) print(drill_b.category)
Drill inherits from Tool, reusing __init__ and use() while adding power_kw. super() calls the parent method – perfect for the open/closed principle.
class Tool: def __init__(self, name: str): self.name = name def use(self) -> str: return f"using {self.name}" class Drill(Tool): def __init__(self, name: str, power_kw: int): super().__init__(name) self.power_kw = power_kw def use(self) -> str: return super().use() + f" with {self.power_kw} kW" hd = Drill("HammerDrill", 300) print(hd.use())
🧠 Design tip: Prefer composition over deep inheritance when behavior varies widely. Mixins can share small pieces of functionality.
Use underscored names (“protected”) and @property to add validation without breaking existing code. Here _celsius is hidden behind a property that rejects unsafe values.
class TemperatureSensor: def __init__(self): self._celsius = 20.0 @property def celsius(self) -> float: return self._celsius @celsius.setter def celsius(self, value: float): if not (-50 <= value <= 150): raise ValueError("Out of safe range") self._celsius = value @property def fahrenheit(self) -> float: return self._celsius * 9/5 + 32 s = TemperatureSensor() s.celsius = 85.0 print(s.fahrenheit)
.action()Python doesn’t need interfaces; any object with an action() method works. This duck typing enables flexible, decoupled code.
class Drill: def action(self): return "drilling rock" class BlastingTeam: def action(self): return "setting explosives" class Hauler: def action(self): return "transporting ore" def perform_actions(equipment_list): for eq in equipment_list: print(eq.action()) team = [Drill(), BlastingTeam(), Hauler()] perform_actions(team)
Implement __str__, __len__, __lt__, __add__ so custom objects work with print(), len(), comparison, and even +.
class OreVein: def __init__(self, mineral: str, grade: float): self.mineral = mineral; self.grade = grade def __str__(self): return f"{self.mineral} ({self.grade}%)" def __len__(self): return int(self.grade * 10) def __lt__(self, other): return self.grade < other.grade def __add__(self, other): return OreVein("mixed", (self.grade + other.grade)/2) v1 = OreVein("gold", 5.2); v2 = OreVein("silver", 3.8) print(v1, len(v1), v1 > v2, v1 + v2)
@classmethod receives the class (cls) and can create instances differently. @staticmethod is just a function attached to the class for organisation.
class Survey: total = 0 def __init__(self, area): self.area = area; Survey.total += 1 @classmethod def from_string(cls, data): return cls(data.split(":")[0]) @classmethod def count(cls): return f"Surveys: {cls.total}" @staticmethod def is_valid_area(name): return len(name) > 3 and name.replace("_","").isalnum() s2 = Survey.from_string("SouthPit:gold") print(Survey.count(), Survey.is_valid_area("West_Zone"))
A DrillingRig contains an Engine and a DrillBit. This makes testing and swapping parts easy.
class Engine: def start(self): return "engine running" class DrillBit: def __init__(self, mat): self.material = mat def spin(self): return f"{self.material} bit spinning" class DrillingRig: def __init__(self, bit_mat): self.engine = Engine(); self.bit = DrillBit(bit_mat) def start_op(self): return f"{self.engine.start()} — {self.bit.spin()}" rig = DrillingRig("diamond") print(rig.start_op())
Using ABC and @abstractmethod you force subclasses to implement operate(). Great for frameworks.
from abc import ABC, abstractmethod class MiningEquipment(ABC): def __init__(self, name): self.name = name @abstractmethod def operate(self): pass class Excavator(MiningEquipment): def operate(self): return f"{self.name} excavating" e = Excavator("CAT 320"); print(e.operate())
@dataclass generates __init__, __repr__, __eq__ automatically. Perfect for data‑centric classes.
from dataclasses import dataclass, field @dataclass class MineralDeposit: name: str depth: float grade: float active: bool = True tags: list = field(default_factory=list) def extractable(self) -> bool: return self.grade > 1.5 and self.depth < 1000 d1 = MineralDeposit("copper", 350.2, 2.3) d2 = MineralDeposit("gold", 820.0, 5.1, tags=["high-grade"]) print(d1, d1.extractable())
When an object has many optional parameters, a builder provides a readable, chainable API.
class DrillConfiguration: def __init__(self, model, power): self.model = model; self.power = power self.depth_cap = 1000; self.bit_type = "standard"; self.autolube = False class DrillBuilder: def __init__(self, model, power): self._config = DrillConfiguration(model, power) def set_depth(self, depth): self._config.depth_cap = depth; return self def set_bit(self, bit): self._config.bit_type = bit; return self def enable_autolube(self): self._config.autolube = True; return self def build(self): return self._config cfg = (DrillBuilder("MegaDrill",800) .set_depth(2500).set_bit("diamond").enable_autolube().build()) print(cfg.bit_type, cfg.autolube)
Metaclasses are the "classes of classes" — they control how classes are built. You can use them to enforce conventions, register subclasses, or add methods automatically. In mining, a metaclass could ensure every piece of equipment has a safety checklist.
class EquipmentMeta(type): def __new__(cls, name, bases, namespace): # Automatically add a safety_inspection method if missing if 'safety_inspection' not in namespace: def safety_inspection(self): return f"⚠️ Basic safety check passed for {self.name}" namespace['safety_inspection'] = safety_inspection return super().__new__(cls, name, bases, namespace) class Drill(metaclass=EquipmentMeta): def __init__(self, name): self.name = name d = Drill("Rotary 77") print(d.safety_inspection())
Metaclasses are powerful but rare — use them when you need to modify classes systematically.
withContext managers handle resource acquisition and release automatically. In a mining operation, opening a tunnel might require ventilation startup and later shutdown. Implement via __enter__/__exit__.
class TunnelVentilation: def __init__(self, tunnel_id): self.tunnel_id = tunnel_id def __enter__(self): print(f"🌀 Ventilation ON for tunnel {self.tunnel_id}") return self # object to use in 'as' def __exit__(self, exc_type, exc_val, exc_tb): print(f"🌀 Ventilation OFF for tunnel {self.tunnel_id}") return False # propagate exceptions if any def read_air_quality(self): return "Air quality: good" with TunnelVentilation("T-12") as tv: print(tv.read_air_quality())
The with statement guarantees cleanup even if an error occurs — essential for safety-critical systems.
Descriptors let you define reusable attribute access (get/set/delete) and share it across classes. Perfect for validated fields like pressure or temperature in mining sensors.
class ValidatedAttribute: def __init__(self, min_val, max_val): self.min = min_val self.max = max_val self.data = "_internal" # name mangled later def __set_name__(self, owner, name): self.private_name = '_' + name # avoid collisions def __get__(self, obj, objtype=None): return getattr(obj, self.private_name) def __set__(self, obj, value): if not (self.min <= value <= self.max): raise ValueError(f"Value {value} out of range [{self.min}, {self.max}]") setattr(obj, self.private_name, value) class HighPressureSensor: pressure = ValidatedAttribute(0, 500) # psi range def __init__(self, pressure): self.pressure = pressure sensor = HighPressureSensor(350) print(sensor.pressure) # sensor.pressure = 600 # would raise ValueError
Descriptors are the machinery behind @property, @staticmethod, and @classmethod.
All examples use mining equipment to make OOP tangible. Reusable, testable, and directly applicable to simulation, control, or data processing in industrial software.