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

  1. Introduction
  2. Learning Objectives
  3. Prerequisites
  4. Class Instantiation: __new__ vs. __init__
  5. Instance, Class, and Static Methods
  6. The Four Pillars of Object-Oriented Programming
  7. Multiple Inheritance & Method Resolution Order (MRO)
  8. Advanced Encapsulation: Getter/Setter Properties
  9. Magic Methods (Dunder Methods)
  10. Visual UML & MRO Flowcharts
  11. Real-World and Production Examples
  12. Best Practices & Common Mistakes
  13. Performance & Security Notes
  14. Interview Insights
  15. Frequently Asked Questions (FAQs)
  16. Summary
  17. 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:


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__name to 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, call super().__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:

  1. 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.

  2. 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.

  3. 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.

  4. 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."
          }
        }
      ]
    }
  ]
}