Python OOP Masterclass: Mastering "Self", Inheritance, and Abstract Classes (2026)
Day 11: The Architecture of State — Classes, OOP, and Dunders
In the wild, code is chaotic. Variables float aimlessly, and functions manipulate data they do not own. To build a legacy, you must bind your data (state) and your functions (behavior) into an unbreakable fortress. This is Object-Oriented Programming (OOP) in Python.
⚠️ The 3 Fatal Beginner Mistakes
Before we build, we must unlearn. Here is how beginners destroy their own architecture when attempting to write classes:
- The Mutable Trap: Defining a list like
inventory = []directly under theclasskeyword. Result: Every user shares the exact same inventory. User A adds an item, and it magically appears in User B's cart. A catastrophic data bleed. - Forgetting
self: Writingdef attack(target):instead ofdef attack(self, target):. Result:TypeError: method takes 1 positional argument but 2 were given. Python secretly passes the object itself as the first argument. If you don't catch it withself, the system crashes. - Returning from
__init__: Trying to writereturn Trueinside the setup function. Result:TypeError: __init__() should return None. The constructor's only job is to build the object, not hand back data.
LET'S UNDERSTAND CLASSES IN PYTHON
From practical blueprints to CPython memory allocation.
▶ Table of Contents 🕉️ (Click to Expand)
- Objects & The Problem Classes Solve
- Anatomy: self, Variables, and Methods
- Parampara: The Lineage of Inheritance
- The Power of super() and Overriding
- Abstract Classes & The Sacred Contract
- Polymorphism & Duck Typing
- The Trinity: Instance vs Class vs Static Methods
- The 10 Dunder (Magic) Methods
- Shielding State: Getters, Setters & @property
- The Modern Era: Dataclasses
- The Deep End: How Classes Work Internally
"Whatever action a great man performs, common men follow. And whatever standards he sets by exemplary acts, all the world pursues." — Bhagavad Gita 3.21
A Class is the great standard. It is the architectural blueprint from which countless identical, yet independent, objects are forged.
1. Objects & The Problem Classes Solve
In Python, everything is an object. A string is an object. An integer is an object. They are blocks of memory containing data, equipped with built-in functions (methods) to manipulate that data.
Why do we need our own Classes?
Imagine building a system to track an army. Without classes, you are forced to pass massive, chaotic dictionaries around your application. You must write separate, disconnected functions to update health or change weapons. If the dictionary structure changes, every function breaks.
2. Anatomy: self, Variables, and Methods
Let us forge our first blueprint. There are two types of data in a class:
- Class Variables: Belong to the Blueprint itself. Shared by every single object created.
- Instance Variables: Belong uniquely to the specific Object. Declared inside
__init__usingself.
What is self?
When you call arjuna.attack(), Python secretly translates this to Warrior.attack(arjuna). The word self is the net that catches the object being passed in, allowing the method to know which specific warrior's data it is supposed to modify.
class Warrior: # CLASS VARIABLE: Shared by all warriors army_name = "Pandava Alliance" # THE CONSTRUCTOR: Initializes the unique state of a new object def __init__(self, name, weapon): # INSTANCE VARIABLES: Unique to 'self' self.name = name self.weapon = weapon self.health = 100 # INSTANCE METHOD: Defines behavior def attack(self, target): print(f"{self.name} strikes {target} with {self.weapon}!") # Forging the Objects (Instances) arjuna = Warrior("Arjuna", "Gandiva Bow") bhima = Warrior("Bhima", "Heavy Mace") # Calling a method arjuna.attack("Kaurava Soldier") # Accessing the shared Class Variable print(f"Both fight for the: {Warrior.army_name}")
[RESULT] Arjuna strikes Kaurava Soldier with Gandiva Bow! Both fight for the: Pandava Alliance
3. Parampara: The Lineage of Inheritance
Inheritance is Parampara—the passing down of knowledge and traits from parent to child. Why rewrite code? A child class absorbs all methods and variables of its parent, reducing duplication.
- Single Inheritance: A child inherits from one parent. (e.g.,
class Archer(Warrior):) - Multi-level Inheritance: A chain of descent. (
Entity→Warrior→Commander) - Multiple Inheritance: A child inherits from two distinct parents simultaneously. Python supports this, unlike Java.
class Charioteer: def drive(self): print("Navigating the battlefield.") class Archer: def shoot(self): print("Releasing arrows.") # MULTIPLE INHERITANCE class Maharathi(Archer, Charioteer): pass # Inherits both skillsets seamlessly karna = Maharathi() karna.drive() karna.shoot()
[RESULT] Navigating the battlefield. Releasing arrows.
4. The Power of super() and Overriding
When a child class defines its own __init__, it overwrites the parent's constructor. To avoid losing the parent's setup logic, we MUST call super(). This dynamically finds the parent class and executes its methods.
Method Overriding occurs when a child class provides a specific implementation of a method that is already provided by its parent class.
class Warrior: def __init__(self, name): self.name = name self.health = 100 def warcry(self): print("Standard roar!") class Commander(Warrior): def __init__(self, name, rank): # MUST DO THIS: Initialize the parent state safely super().__init__(name) self.rank = rank # Add child-specific state # OVERRIDING the parent method def warcry(self): print(f"The {self.rank} demands silence!") drona = Commander("Drona", "Supreme General") print(f"Health inherited: {drona.health}") drona.warcry()
[RESULT] Health inherited: 100 The Supreme General demands silence!
5. Abstract Classes & The Sacred Contract
Sometimes, a parent class is purely conceptual. You should never be able to instantiate a raw "Shape" or a generic "Warrior"—you should only instantiate specific implementations like "Circle" or "Archer".
An Abstract Base Class (ABC) enforces a contract. It forces all child classes to write their own version of a specific method before they are allowed to exist.
from abc import ABC, abstractmethod class Weapon(ABC): @abstractmethod def deal_damage(self): pass class Sword(Weapon): # If Sword does not write 'deal_damage', Python will crash upon creation. def deal_damage(self): print("Slashing for 50 damage!") # generic = Weapon() -> TypeError: Can't instantiate abstract class blade = Sword() blade.deal_damage()
6. Polymorphism & Duck Typing
Polymorphism means "many forms." It allows different classes to have methods with the exact same name, allowing a single function to process totally different objects seamlessly.
In strongly typed languages (Java), an object must explicitly inherit from a specific interface to be valid. Python uses Duck Typing: "If it walks like a duck and quacks like a duck, it must be a duck." Python does not care about the object's lineage; it only checks if the required method exists at the exact moment it is called.
class Elephant: def advance(self): print("Trampling forward.") class Cavalry: def advance(self): print("Galloping fast.") # POLYMORPHISM & DUCK TYPING IN ACTION # Python doesn't care what 'unit' is, as long as it has an 'advance' method. def command_charge(unit): unit.advance() command_charge(Elephant()) command_charge(Cavalry())
[RESULT] Trampling forward. Galloping fast.
7. The Trinity: Instance vs Class vs Static Methods
| Method Type | First Argument | Use Case |
|---|---|---|
| Instance Method (Default) | self |
Modifying or reading data unique to a specific object. |
| @classmethod | cls |
Modifying shared Class Variables, or creating "Alternative Constructors" (factory patterns). |
| @staticmethod | None | Utility functions that logically belong in the class but do not need access to self or cls data. |
class MathEngine: pi = 3.14159 @classmethod def update_pi(cls, new_pi): cls.pi = new_pi # Modifies state for ALL instances @staticmethod def add(a, b): # Needs neither 'self' nor 'cls'. Just a logical grouping. return a + b
8. The 10 Dunder (Magic) Methods
Dunder (Double UNDERscore) methods allow your custom objects to interact with Python's built-in syntax (like +, ==, or len()). Without them, your objects are mute. With them, your objects become native Python citizens.
__init__(self): The Constructor. Initializes memory state.__str__(self): Returns a human-readable string when youprint(object).__repr__(self): Returns a technical string for developers (used in logging/debugging).__eq__(self, other): Defines what happens when you use==between two objects.__lt__(self, other): Defines<(Less Than). Crucial if you want to.sort()a list of your objects!__add__(self, other): Defines what happens when you use the+operator.__len__(self): Defines what is returned when you calllen(object).__getitem__(self, index): Allows indexing likeobject[0]or dictionary keys.__call__(self): Allows an instance to be executed like a functionobject().__del__(self): The Destructor. Triggers right before the garbage collector destroys the object in RAM.
class GoldCoin: def __init__(self, weight): self.weight = weight def __add__(self, other): # Overloading the '+' operator to fuse two coins return GoldCoin(self.weight + other.weight) def __str__(self): return f"Coin({self.weight}g)" coin1 = GoldCoin(10) coin2 = GoldCoin(5) huge_coin = coin1 + coin2 # Triggers __add__ print(huge_coin) # Triggers __str__
[RESULT] Coin(15g)
9. Shielding State: Getters, Setters & @property
In Java, developers write massive getKarma() and setKarma() functions. In Python, this is considered an anti-pattern. We prefer direct attribute access (user.karma). However, what if we need to validate the data to ensure karma never drops below zero?
We use the @property decorator. It acts like a variable when accessed, but executes like a function under the hood, protecting the internal state without breaking the external API.
class Soul: def __init__(self): self._karma = 100 # The underscore implies it is "private" @property def karma(self): # The Getter return self._karma @karma.setter def karma(self, value): # The Setter: Validation Logic if value < 0: print("Error: Karma cannot be negative.") else: self._karma = value human = Soul() human.karma = -50 # Triggers the setter validation! print(human.karma) # Still 100
[RESULT] Error: Karma cannot be negative. 100
10. The Modern Era: Dataclasses
Writing __init__, __str__, and __eq__ for every class that just holds data is exhausting boilerplate. Introduced in Python 3.7, @dataclass automatically writes these Dunder methods for you via metaprogramming.
from dataclasses import dataclass @dataclass class WeaponRecord: name: str damage: int is_legendary: bool = False # __init__ and __str__ are automatically generated! astra = WeaponRecord("Brahmastra", 9999, True) print(astra)
[RESULT] WeaponRecord(name='Brahmastra', damage=9999, is_legendary=True)
11. The Deep End: How Classes Work Internally
We must pierce the final veil of Maya. At the CPython level, there is no magic. A Class is not a metaphysical concept; it is a highly optimized Dictionary attached to an executable type object.
The Matrix of __dict__
When you write arjuna.weapon = "Bow", Python does not create a dedicated memory register for a "weapon". It secretly stores this string inside a hidden dictionary attached to the object, called __dict__. You can literally bypass standard OOP syntax and hack the dictionary directly.
class Warrior: army = "Pandava Alliance" def __init__(self, name): self.name = name arjuna = Warrior("Arjuna") # 1. Viewing the internal state print(f"Object Memory: {arjuna.__dict__}") print(f"Class Memory contains army: {'army' in Warrior.__dict__}") # 2. Hacking the Matrix directly arjuna.__dict__['weapon'] = "Gandiva" print(f"Direct access: {arjuna.weapon}")
[RESULT]
Object Memory: {'name': 'Arjuna'}
Class Memory contains army: True
Direct access: Gandiva
⚙️ The MRO (Method Resolution Order)
When you call a method or variable like arjuna.attack(), Python performs a brutal lookup sequence using the C3 Linearization algorithm:
- Checks the instance's
__dict__. - If not found, it checks the Class's
__dict__. - If not found, it traverses up the Inheritance Tree sequentially.
- If it reaches the base
objectclass and finds nothing, it throws anAttributeError.
# Unveiling the Lineage (MRO) in Multiple Inheritance class Deity: pass class Human: pass class DemiGod(Deity, Human): pass # The .mro() method reveals the exact search path Python uses. print([cls.__name__ for cls in DemiGod.mro()])
[RESULT] ['DemiGod', 'Deity', 'Human', 'object']
Classes Are Objects Too (Metaclasses)
If arjuna is an object created by the Warrior class, who created the Warrior class? Classes are objects created by Python's internal type class. Because classes are just objects in RAM, you can forge them dynamically during runtime without ever using the class keyword.
# Forging a Class purely out of raw data # type('ClassName', (ParentClasses,), {'dictionary_of_attributes'}) def dynamic_attack(self): return "A strike from the void!" # We create the class dynamically PhantomWarrior = type('PhantomWarrior', (object,), { 'health': 500, 'attack': dynamic_attack }) # Instantiate the dynamic class ghost = PhantomWarrior() print(f"Ghost Health: {ghost.health}") print(ghost.attack())
[RESULT] Ghost Health: 500 A strike from the void!
This is the absolute bleeding edge. When you control how classes themselves are created (using Metaclasses), you control the very fabric of your application's physics.
12. The Forge: The Army Roster (Challenge)
The Challenge: Theory is useless without execution. Build an ArmyRoster using the modern @dataclass. You must implement the __len__ dunder method so we can call len(roster), and forge a @classmethod factory that generates a default Pandava army.
from dataclasses import dataclass, field from typing import List @dataclass class Warrior: name: str power: int @dataclass class ArmyRoster: faction: str warriors: List[Warrior] = field(default_factory=list) # TODO: Implement the __len__ dunder method # TODO: Implement a @classmethod factory named 'create_pandavas' # Execution test: # my_army = ArmyRoster.create_pandavas() # print(f"Troop count: {len(my_army)}")
💡 Production Standard Upgrade
Elevate this architecture by adding:
- The
__add__dunder method so you can merge two armies usingarmy_3 = army_1 + army_2. - The
__getitem__dunder method so you can select a warrior directly usingmy_army[0]instead ofmy_army.warriors[0].
13. The Vyuhas – Key Takeaways
- The Mutable Trap: Never assign lists or dictionaries directly inside a class body. Initialize them inside
__init__usingselfto prevent data bleeding across users. - Duck Typing over Lineage: Python cares about what an object can do, not what it is. If it has an
attack()method, it can be passed into any function expecting an attacker. - Dunder Magic: Double-underscore methods (
__len__,__add__) are the secret interface that allows your custom objects to seamlessly integrate with native Python syntax. - Shielding State: Never use Java-style get/set methods. Use the
@propertydecorator to validate internal state while preserving cleanobject.attributesyntax. - The MRO Matrix: Method Resolution Order follows the C3 Linearization algorithm. Python checks the instance, then the class, then traverses the parent hierarchy sequentially.
FAQ: Classes, OOP & Metaclasses
Architectural OOP questions answered — optimised for quick lookup.
What is the difference between `__str__` and `__repr__`?
__str__ is for the end-user. It returns a clean, human-readable string when you use print(obj). __repr__ is for the developer. It should ideally return a string that represents the exact Python code required to recreate the object (e.g., Warrior(name='Arjuna')). If __str__ is missing, Python falls back to __repr__.
When should I use @classmethod vs @staticmethod?
@classmethod when your method needs to interact with the Class itself (modifying shared class variables or building factory constructors like create_from_json(cls, data)). Use @staticmethod when your method is just a standard utility function that logically belongs inside the class blueprint but requires no access to self or cls data to function.
What is Duck Typing in Python?
.swim(), Python doesn't care if the object is a Fish or a Human, as long as the swim() method exists.
What is the MRO (Method Resolution Order)?
ClassName.mro().
What is a Metaclass in Python?
type. A metaclass intercepts the creation of a class blueprint in memory, allowing senior architects to automatically inject methods, enforce coding standards, or modify class variables across entire frameworks before the class even finishes loading into RAM.
The Infinite Game: Join the Vyuha
If you are building an architectural legacy, hit the Follow button in the sidebar to receive the remaining days of this 30-Day Series directly to your feed.
💬 Have you ever fallen into the Mutable Default Argument trap? Drop your bug-hunt war story in the comments below.
The Architect's Protocol: To master the architecture of logic, read The Architect's Intent.

Comments
Post a Comment