2.3Object-oriented programming (classes, inheritance, dunder)
The essentials in 30 seconds
- The class is the mold, the object is the piece: attributes (data) + methods (verbs on self) travel together — no more scattered dicts + functions.
- Inheritance = is-a: the child specializes the parent via super(), kept shallow (≤ 3) — composition for has-a.
- Dunders = first-class objects: __str__/__eq__/__len__ answer print/==/len — @dataclass for data holders.
Running mini-case — Nabil models his sales as objects
Nabil's dicts scatter: functions handling sales without ever checking amounts, reports duplicating the same computations. He models: a Vente class (wilaya, montant, total and readable __str__ methods), a RapportMensuel class grouping sales, then a VenteRemise class that INHERITS from Vente and adds a rate (super().__init__() reuses the parent). No more scattered dicts + functions: data and behaviors live together. In 2.4, creation will reject negative amounts with raise.
Sequel of the 2.2 running thread: Nabil's rows become objects — and continue in 2.4.
1. The class: the mold, the object: the piece
class Vente: + __init__(self, ...) defines the mold; v = Vente("Algiers", 1200) creates the piece. self means the object itself inside its methods: each object accesses ITS attributes (self.montant) independently of the others. Attributes are the data, methods are the behaviors (verbs on self, never globals). Data + behaviors travel together: no more scattered dictionaries + functions. Nabil creates two distinct sales that never mix.
2. Inheritance: specialize, don't copy
class VenteRemise(Vente): inherits and specializes (rate, discounted computation). super().__init__() reuses the parent constructor instead of copying it; redefining a method in the child is polymorphism. Rule: inheritance for "is-a" (a VenteRemise is-a Vente), composition ("has-a": a Rapport has Ventes) otherwise. Three levels max: beyond that, it is a maze. Nabil adds discounted sales without touching existing code.
3. Dunder: speaking fluent Python
Dunder (double underscore) methods plug your objects into native mechanisms: __str__ for readable display (print(v)), __eq__ for equality (==), __len__ for size (len()). Without them, print(objet) shows a cryptic <... object at 0x...>. For simple data holders, @dataclass generates __init__, __eq__ and __repr__ automatically. Nabil's objects thus answer print() and == like native types.
🔎 Concrete example — 🔎 Concrete example — Nabil's sales that display themselves
Readable and comparable objects, as at Nabil's:
class Vente:
def __init__(self, wilaya, montant):
self.wilaya = wilaya
self.montant = float(montant)
def __str__(self):
return f"{self.wilaya} : {self.montant:.2f} €"
class VenteRemise(Vente):
def __init__(self, wilaya, montant, taux=0.1):
super().__init__(wilaya, montant)
self.taux = taux
def net(self):
return self.montant * (1 - self.taux)
v = VenteRemise("Algiers", 1000)
print(v) # Algiers : 1000.00 €
print(f"Net : {v.net():.2f} €") # Net : 900.00 €
Reading: __init__ initializes, __str__ displays, super() reuses the parent.
Lesson: methods = verbs on self, never globals.
Section vocabulary
- Class / object
- The class is the plan (mold); the object (instance) is a concrete realization created from that plan: v = Vente(...).
Simple example: Nabil creates two distinct sales from the same class.
What it is not: a free dictionary with no rules or behaviors.
Reflex: concept to model = class.
- Attribute / method
- The attribute is object data (self.montant); the method is a function attached to the object, a verb on self.
Simple example: Nabil's sale carries wilaya and computes net().
What it is not: global functions scattered far from data.
Reflex: verb on data = method.
- __init__ / self
- __init__ is the constructor, called at creation; self means the current object, written in the definition but passed automatically at the call.
Simple example: Vente("Algiers", 1200) initializes self.wilaya and self.montant.
What it is not: a parameter to pass by hand at each call.
Reflex: creating an object = __init__ via self.
- Inheritance
- A child class inherits a parent's attributes and methods and specializes them; super() reuses the parent. Reserved for "is-a", kept shallow.
Simple example: VenteRemise inherits from Vente and adds the rate.
What it is not: a copy-paste of the parent class.
Reflex: is-a = inheritance; has-a = composition.
- Dunder method
- Double-underscore method (__str__, __len__, __eq__) that plugs your objects into print(), ==, len(); @dataclass generates them for data holders.
Simple example: print(vente) displays "Algiers : 1000.00 €" thanks to __str__.
What it is not: a cryptic
Reflex: displayable/comparable object = dunder.
In practice — In practice — Creating classes and inheritance
- Define a Vente class with __init__(self, wilaya, montant) and a display() method. Create two distinct objects and check their independence.
- Add __str__ for readable display (print(v) → "Algiers : 1000.00 €") and __eq__ to compare two sales.
- Create a VenteRemise class that INHERITS from Vente, adds a rate and a net() method. Reuse the parent with super().__init__().
- Redefine net() in the child (polymorphism) and test @dataclass on a simple data-holder class.
Script ready to say — defending the class to the team (30 s)
"For Nabil's sales, I replaced scattered dicts with a Vente class: wilaya and montant initialized in __init__, computations as methods on self — each object is independent. Discounts inherit with super(), and __str__ gives a clean display. We no longer touch data without going through the object."
Say it while showing a tinkered dict then the same case as a class.
Copy-paste template — class, inheritance and dunder
class Vente:
def __init__(self, wilaya, montant):
if float(montant) < 0:
raise ValueError("negative amount forbidden")
self.wilaya = wilaya
self.montant = float(montant)
def __str__(self):
return f"{self.wilaya} : {self.montant:.2f} €"
def __eq__(self, autre):
if not isinstance(autre, Vente):
return NotImplemented
return (self.wilaya, self.montant) == (autre.wilaya, autre.montant)
class VenteRemise(Vente):
def __init__(self, wilaya, montant, taux=0.1):
super().__init__(wilaya, montant)
self.taux = taux
def net(self):
return self.montant * (1 - self.taux)
Simple sale
One of Nabil's Vente carries wilaya + montant and displays as an f-string via __str__.
Discounted sale
VenteRemise inherits and adds the rate: super() reuses the parent, net() computes.
Monthly report
The Rapport has Ventes (composition): it totals without inheriting — has-a, not is-a.
Data holder
A simple summary becomes a @dataclass: __init__ and __eq__ generated, zero repetitive code.
✓ Best practices
- Group data + behaviors in a class (never dict + scattered functions).
- Inherit shallowly (≤ 3) for is-a; compose for has-a; super() for reuse.
- Add __str__/__eq__ and @dataclass for first-class objects.
✗ Common pitfalls
- Keeping everything in dict + scattered global functions.
- Using inheritance for "has-a" instead of composition.
- Stacking 6 unreadable inheritance levels.
Key takeaways
- The class is the mold; the object (instance) is one realization of it.
- __init__ initializes the object; self means the current object (passed automatically).
- Inheritance lets a child class reuse/extend a parent class (is-a, super()).
- Dunder methods (__str__…) plug your objects into Python's mechanisms; @dataclass for data holders.
48h challenge — apply tomorrow
Model a real concept (Nabil's or yours) as a class with __init__ + __str__, create two independent objects, add a child class with super() and test equality with __eq__.
- I can explain class vs object and the role of self.
- I can create a class, a method and an inheritance with super().
- I have made my objects readable with __str__ (and @dataclass where suitable).
Frequently asked questions
What exactly is self for?
self represents the object on which the method is called. It lets each object access ITS own attributes (self.montant) independently of the others. Python passes it automatically at the call: you write it in the definition, not at the call.
Is OOP essential for beginners?
For small scripts, no: functions are enough. But as soon as programs grow — and to understand libraries (pandas, Django…) that use it everywhere — OOP becomes essential. Investing here pays off long term.
What does "self" mean in a method?