Python Object-Oriented Programming: Inheritance, ABCs, and Metaprogramming
Master Python OOP from basics to advanced. Learn about classes, object instantiation (__new__ vs __init__), C3 MRO diamond inheritance, polymorphism with ABCs, property decorators, and slots memory optimization.
Table of Contents
- Introduction
- Learning Objectives
- Prerequisites
- Class Instantiation:
__new__vs.__init__ - Instance, Class, and Static Methods
- The Four Pillars of Object-Oriented Programming
- Multiple Inheritance & Method Resolution Order (MRO)
- Advanced Encapsulation: Getter/Setter Properties
- Magic Methods (Dunder Methods)
- Visual UML & MRO Flowcharts
- Real-World and Production Examples
- Best Practices & Common Mistakes
- Performance & Security Notes
- Interview Insights
- Frequently Asked Questions (FAQs)
- Summary
- Related Tutorials
Introduction
Object-Oriented Programming (OOP) is a programming paradigm centered around data, or "objects," rather than logic and actions. It allows developers to bind state (attributes) and behavior (methods) into cohesive, reusable structures.
Python is a multi-paradigm language that supports OOP. In fact, everything in Python is an object—including integers, strings, functions, and modules.
However, Python's object model is distinct from languages like Java or C++:
- It uses Duck Typing for polymorphism rather than strict interface matching.
- It resolves multiple inheritance hierarchies using the C3 Linearization algorithm.
- It implements private attributes through Name Mangling rather than compiler-enforced access modifiers.
This guide provides a detailed look at Python's OOP system, covering instantiation pipelines, inheritance hierarchies, abstract base classes, property decorators, and class memory optimizations.
Learning Objectives
By the end of this tutorial, you will be able to:
- Differentiate between the role of
__new__(allocating memory) and__init__(initializing state) in object creation. - Implement and select appropriate contexts for Instance, Class (
@classmethod), and Static (@staticmethod) methods. - Apply the four pillars of OOP (Inheritance, Polymorphism, Encapsulation, Abstraction) to build clean codebase structures.
- Resolve multiple inheritance structures and construct custom Abstract Base Classes (ABCs).
- Use property decorators (
@property) to build clean getter/setter APIs. - Optimize class memory usage and speed up attribute access using
__slots__.
Prerequisites
Before starting this tutorial, make sure you understand:
- Python Functions — namespaces, scopes, and decorators.
- Python Collections — lists, tuples, dictionaries, and key-value mapping.
Class Instantiation: __new__ vs. __init__
Most developers believe __init__ is the constructor of a Python class. In reality, __new__ is the true constructor, while __init__ is an initializer.
graph TD
A[Call: ClassNameNameargs] --> B[__new__ Class, args]
B -->|Allocates memory and creates| C[Instance Object]
C --> D[__init__ self, args]
D -->|Initializes attributes| E[Returned Fully Configured Instance]
1. __new__(cls, *args, **kwargs)
A static method responsible for creating and returning a new instance of the class. It is called before __init__. You override __new__ when subclassing immutable types (like int or str) or when implementing design patterns like the Singleton.
2. __init__(self, *args, **kwargs)
An instance method responsible for initializing the newly created object's state (attributes). It returns None.
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
# Implement Singleton Pattern: ensure only one instance is ever created
if cls._instance is None:
print("Allocating memory for Singleton object...")
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, name):
print("Initializing Singleton attributes...")
self.name = name
s1 = Singleton("Instance A")
s2 = Singleton("Instance B") # Returns same instance, re-initializes name
print(s1 is s2) # Output: True (Points to the exact same object)
Instance, Class, and Static Methods
Methods inside a Python class can be classified into three types based on the context they access:
| Method Type | Decorator | First Parameter | Access Level | Use Case |
| :--- | :--- | :--- | :--- | :--- |
| Instance Method | None | self | Accesses instance state (self) and class state | Read/write object attributes |
| Class Method | @classmethod | cls | Accesses class state (cls) only | Factory methods (alternative constructors) |
| Static Method | @staticmethod | None | No access to instance or class state | Utility functions isolated within class |
class DateProcessor:
def __init__(self, year: int, month: int, day: int):
self.year = year
self.month = month
self.day = day
# 1. Instance Method
def get_formatted_date(self) -> str:
return f"{self.year:04d}-{self.month:02d}-{self.day:02d}"
# 2. Class Method (Factory Constructor)
@classmethod
def from_string(cls, date_str: str) -> "DateProcessor":
# Parses "YYYY-MM-DD" and instantiates class
parts = list(map(int, date_str.split("-")))
return cls(parts[0], parts[1], parts[2])
# 3. Static Method (Utility)
@staticmethod
def is_valid_year(year: int) -> bool:
return 1900 <= year <= 2100
# Usage
d1 = DateProcessor.from_string("2026-06-19")
print(d1.get_formatted_date()) # Output: "2026-06-19"
print(DateProcessor.is_valid_year(2026)) # Output: True
The Four Pillars of Object-Oriented Programming
1. Inheritance
Enables a child class to inherit attributes and methods from a parent class, promoting code reuse:
class Vehicle:
def __init__(self, brand: str):
self.brand = brand
def start_engine(self):
print(f"The {self.brand} engine is starting...")
class ElectricCar(Vehicle):
def __init__(self, brand: str, battery_capacity: int):
super().__init__(brand) # Invokes parents constructor
self.battery_capacity = battery_capacity
tesla = ElectricCar("Tesla", 100)
tesla.start_engine() # Output: The Tesla engine is starting...
2. Polymorphism (Duck Typing & ABCs)
Polymorphism allows different classes to share interface names.
In Python, this is traditionally driven by Duck Typing: "If it walks like a duck and quacks like a duck, it's a duck." The interpreter only checks that a method is present on the object at runtime, rather than requiring a specific class type:
class Cat:
def speak(self):
return "Meow!"
class Dog:
def speak(self):
return "Woof!"
def make_animal_speak(animal_instance):
# Runs successfully as long as animal_instance has a speak() method
print(animal_instance.speak())
make_animal_speak(Cat()) # Output: Meow!
make_animal_speak(Dog()) # Output: Woof!
For strict enforcement of interfaces, use Abstract Base Classes (ABCs):
from abc import ABC, abstractmethod
class PaymentGateway(ABC):
@abstractmethod
def process_payment(self, amount: float):
"""Must be implemented by subclasses."""
pass
class StripePayment(PaymentGateway):
def process_payment(self, amount: float):
print(f"Processing ${amount} via Stripe.")
# stripe = PaymentGateway() # Raises TypeError: Can't instantiate abstract class
gateway = StripePayment()
gateway.process_payment(50.0)
3. Encapsulation & Access Modifiers
Encapsulation restricts direct access to an object's components, shielding its internal state.
Python uses naming conventions to manage access:
- Public: Access is unrestricted.
- Protected (
_name): A warning convention indicating the attribute should not be accessed outside the class hierarchy. - Private (
__name): Triggers Name Mangling. The interpreter renames the attribute to_ClassName__nameto prevent accidental access.
class ConfidentialRecord:
def __init__(self, key: str):
self.__key = key # Private attribute
record = ConfidentialRecord("secret_123")
# print(record.__key) # Raises AttributeError
# Accessing via name mangled path (Not recommended in production)
print(record._ConfidentialRecord__key) # Output: "secret_123"
4. Abstraction
Hiding background execution details and exposing only what is necessary, achieved using class structures and interfaces.
Multiple Inheritance & Method Resolution Order (MRO)
Python supports multiple inheritance, allowing a class to inherit from multiple parent classes.
The Diamond Problem and C3 Linearization
When class D inherits from B and C, and both B and C inherit from A, a conflict arises: if A defines a method that both B and C override, which version should D inherit?
A
/ \
B C
\ /
D
Python resolves this conflict using the C3 Linearization algorithm to compute the Method Resolution Order (MRO). The MRO guarantees that:
- Subclasses are always searched before their parent classes.
- If a class inherits from multiple parents, the parents are searched in the order listed in the class definition.
class A:
def speak(self):
print("Speaker: A")
class B(A):
def speak(self):
print("Speaker: B")
class C(A):
def speak(self):
print("Speaker: C")
class D(B, C):
pass
d = D()
d.speak() # Output: Speaker: B (Because B is searched before C)
# View the full Method Resolution Order search path
print(D.__mro__)
# Output: (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
Advanced Encapsulation: Getter/Setter Properties
Instead of writing verbose Java-style getter and setter methods (e.g., getBalance() and setBalance()), Python uses the @property decorator to expose attributes with clean, field-like access:
class TemperatureSensor:
def __init__(self, celsius: float):
self._celsius = celsius
# Getter property
@property
def celsius(self) -> float:
return self._celsius
# Setter property
@celsius.setter
def celsius(self, value: float):
if value < -273.15:
raise ValueError("Temperature below absolute zero is impossible!")
self._celsius = value
sensor = TemperatureSensor(25.0)
print(sensor.celsius) # Getter runs: 25.0
sensor.celsius = 30.0 # Setter runs
# sensor.celsius = -300.0 # Raises ValueError
Magic Methods (Dunder Methods)
Magic methods (or dunder methods, short for double underscore) allow your custom classes to integrate with Python's built-in syntax.
| Magic Method | Triggers When... | Example Context |
| :--- | :--- | :--- |
| __str__(self) | Object is converted to string for users | print(obj) |
| __repr__(self) | Detailed developer-friendly representation | Interactive shell output |
| __len__(self) | Length of object is queried | len(obj) |
| __eq__(self, other)| Value equality check is run | obj1 == obj2 |
| __call__(self) | Object is called like a function | obj() |
class Book:
def __init__(self, title: str, author: str, pages: int):
self.title = title
self.author = author
self.pages = pages
def __str__(self) -> str:
return f"'{self.title}' by {self.author}"
def __repr__(self) -> str:
return f"Book(title='{self.title}', author='{self.author}', pages={self.pages})"
def __len__(self) -> int:
return self.pages
book = Book("1984", "George Orwell", 328)
print(str(book)) # Output: '1984' by George Orwell
print(repr(book)) # Output: Book(title='1984', author='George Orwell', pages=328)
print(len(book)) # Output: 328
Visual UML & MRO Flowcharts
Diamond Multiple Inheritance MRO Graph
This diagram shows the search path Python takes to resolve methods in the Diamond Problem:
graph TD
D[Class D Instance] -->|1. Searches| B[Class B]
B -->|2. Searches| C[Class C]
C -->|3. Searches| A[Class A]
A -->|4. Searches| Obj[class object]
style B fill:#d4edda,stroke:#28a745
style C fill:#d4edda,stroke:#28a745
Real-World and Production Examples
Example 1: Database Repository Abstraction Interface
In production software, abstract base classes are used to define contracts for data storage. This allows you to swap out database backends (e.g., PostgreSQL for MongoDB) without modifying your application logic.
from abc import ABC, abstractmethod
from typing import Dict, Any
class UserRepository(ABC):
@abstractmethod
def save(self, user_id: int, data: Dict[str, Any]):
pass
@abstractmethod
def find_by_id(self, user_id: int) -> Dict[str, Any]:
pass
class InMemoryUserRepository(UserRepository):
def __init__(self):
self._storage: Dict[int, Dict[str, Any]] = {}
def save(self, user_id: int, data: Dict[str, Any]):
self._storage[user_id] = data
print(f"Saved user {user_id} in-memory.")
def find_by_id(self, user_id: int) -> Dict[str, Any]:
return self._storage.get(user_id, {})
# Client execution
repo: UserRepository = InMemoryUserRepository()
repo.save(1, {"username": "developer_bob", "email": "bob@test.com"})
print(repo.find_by_id(1))
Example 2: Optimizing Class Instances Memory Using Slots
By default, Python stores instance attributes in a dictionary named __dict__. This dictionary enables dynamic addition of attributes at runtime but introduces significant memory overhead.
If you plan to instantiate millions of small data objects, use __slots__ to specify a fixed set of attributes, eliminating __dict__ and reducing memory usage:
import sys
class DynamicPoint:
def __init__(self, x, y):
self.x = x
self.y = y
class SlottedPoint:
# Restrict attributes to x and y; removes __dict__
__slots__ = ["x", "y"]
def __init__(self, x, y):
self.x = x
self.y = y
dp = DynamicPoint(1, 2)
sp = SlottedPoint(1, 2)
# Slotted classes do not support adding dynamic attributes at runtime
try:
sp.z = 10 # Raises AttributeError
except AttributeError as e:
print(f"Blocked: {e}")
# Compare structure sizes in memory
print(f"Dynamic instance size: {sys.getsizeof(dp)} bytes + __dict__: {sys.getsizeof(dp.__dict__)} bytes")
print(f"Slotted instance size: {sys.getsizeof(sp)} bytes (No __dict__ overhead)")
Best Practices & Common Mistakes
Best Practices
- Prefer Composition Over Inheritance: Avoid building deep inheritance hierarchies. Instead, design classes that delegate tasks to other components ("has-a" relationship) rather than subclassing them ("is-a" relationship).
- Always call
super()inside__init__: When overriding parent constructors, callsuper().__init__()to ensure that parent classes are initialized correctly.
Common Mistakes
- Shadowing Class Attributes with Instance Attributes: Modifying a class attribute on an instance creates a new instance attribute with the same name, hiding the class attribute for that instance:
class Connection: port = 8080 # Class Attribute c1 = Connection() c2 = Connection() # Modifying port via instance c1 c1.port = 9090 # Creates instance attribute c1.port; c2.port remains 8080! print(c1.port, c2.port) # Output: 9090, 8080 - Overusing Private Attributes (
__): Do not mark every attribute private using double underscores. In Python, prefer single underscores (_) to indicate protected variables, unless name mangling is explicitly required to prevent name collisions in base classes.
Performance & Security Notes
Instantiation Performance Analysis
If your application instantiates objects in tight loops, using __slots__ can speed up attribute access times by up to 20% by avoiding dictionary lookups.
Security: Bypassing Name Mangling
Name mangling is not a security boundary; it is a design guardrail. An attacker can still read and write private attributes:
class SecuredAccess:
def __init__(self, key):
self.__secret_key = key
sec = SecuredAccess("admin_password")
# Bypassing the private access restriction
print(sec._SecuredAccess__secret_key) # Output: "admin_password"
Do not rely on name mangling to secure sensitive API credentials or cryptographic keys in memory.
Interview Insights
Typical Interview Questions:
-
What is the difference between
__init__and__new__? Answer Key:__new__is a static method responsible for allocating memory and returning a new instance of the class.__init__is an instance method responsible for initializing the attributes of the newly created instance. -
How does Python resolve Multiple Inheritance Diamond problems? Answer Key: Python resolves multiple inheritance using the C3 Linearization algorithm to compute the Method Resolution Order (MRO). The MRO defines the order in which parent classes are searched, ensuring children are searched before parents and preserving the order defined in the class signature.
-
What is the purpose of
__slots__? Answer Key:__slots__is a class-level variable that restricts instance attributes to a predefined set. By eliminating the default instance dictionary__dict__, it reduces the memory footprint of objects and speeds up attribute access. -
What is Duck Typing? Answer Key: Duck typing is Python's dynamic implementation of polymorphism. It states that an object's suitability is determined by the presence of specific methods or properties at runtime, rather than its inheritance lineage.
Frequently Asked Questions (FAQs)
Q: Can we implement method overloading in Python?
A: Python does not natively support defining multiple methods with the same name but different parameter types in a class. The last defined method overwrites any previous definitions. To support multiple argument configurations, use default arguments or variable-length arguments (*args, **kwargs).
Q: Why does Python require self to be passed explicitly in methods?
A: Python's design philosophy prioritizes simplicity and explicitness ("Explicit is better than implicit"). Requiring self distinguishes instance attributes from local variables, ensuring that method parameters are handled consistently.
Summary
Object-Oriented Programming in Python is flexible and dynamic. By mastering class instantiation (__new__ vs. __init__), method resolution order (MRO), abstract base classes (ABCs), and memory optimizations like __slots__, you can build clean, performant, and maintainable object models.
Related Tutorials
Technical SEO Schema Metadata
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Article",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://vsnexos.com/placement-prep/python/classes-objects"
},
"headline": "Python Object-Oriented Programming: Inheritance, ABCs, and Metaprogramming",
"description": "Master Python OOP. Explore class instantiation, Method Resolution Order, Abstract Base Classes, property getters/setters, andslots memory optimization.",
"image": "https://vsnexos.com/images/courses/python-oop.png",
"author": {
"@type": "Organization",
"name": "Vsnexos"
},
"publisher": {
"@type": "Organization",
"name": "Vsnexos",
"logo": {
"@type": "ImageObject",
"url": "https://vsnexos.com/images/logo.png"
}
}
},
{
"@type": "Course",
"name": "Python Zero to Hero Master Course",
"description": "A comprehensive course to transform beginners into industry-ready Python developers.",
"provider": {
"@type": "Organization",
"name": "Vsnexos",
"sameAs": "https://vsnexos.com"
}
},
{
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://vsnexos.com"
},
{
"@type": "ListItem",
"position": 2,
"name": "Placement Prep",
"item": "https://vsnexos.com/placement-prep"
},
{
"@type": "ListItem",
"position": 3,
"name": "Python Course",
"item": "https://vsnexos.com/placement-prep/python"
},
{
"@type": "ListItem",
"position": 4,
"name": "OOP and Classes",
"item": "https://vsnexos.com/placement-prep/python/classes-objects"
}
]
},
{
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is C3 Linearization in Python?",
"acceptedAnswer": {
"@type": "Answer",
"text": "C3 Linearization is the algorithm Python uses to compute the Method Resolution Order (MRO) in multiple inheritance, ensuring subclasses are searched before parent classes and resolving diamond inheritance conflicts."
}
},
{
"@type": "Question",
"name": "How do slots improve performance in Python classes?",
"acceptedAnswer": {
"@type": "Answer",
"text": "slots eliminates the default __dict__ namespace dictionary for instances, allocating a fixed set of attribute slots instead. This reduces memory footprint and speeds up attribute access."
}
},
{
"@type": "Question",
"name": "Can we prevent subclass instantiation of an Abstract Class?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes, by inheriting from abc.ABC and marking at least one method with the @abstractmethod decorator, Python raises a TypeError if a user attempts to instantiate the abstract class directly."
}
}
]
}
]
}