Concept #160Mediumpython-for-gen-aiimportant

Explain me detailly about the Oops concept in Python?

#python#oop#classes#inheritance#encapsulation#abstraction#polymorphism

Answer

Object-Oriented Programming (OOP) in Python

OOP is a programming paradigm that organises code into objects - combining data (attributes) and behaviour (methods). Python supports OOP natively and uses it heavily across Gen AI frameworks like LangChain, PyTorch, and Hugging Face.


The 4 Pillars of OOP

PillarDefinitionPython Mechanism
EncapsulationBundle data + behaviour; restrict direct access
text
_protected
,
text
__private
, properties
AbstractionHide complexity, expose only what's needed
text
abc.ABC
,
text
@abstractmethod
InheritanceChild class reuses/extends parent class
text
class Child(Parent)
PolymorphismSame interface, different behaviourMethod overriding, duck typing

1. Classes & Objects

A class is a blueprint; an object is an instance of that blueprint.

python
class LLMModel:
    # Class variable - shared across all instances
    provider = "OpenAI"

    def __init__(self, name: str, max_tokens: int):
        # Instance variables - unique to each object
        self.name = name
        self.max_tokens = max_tokens

    def describe(self) -> str:
        return f"{self.name} (max_tokens={self.max_tokens})"

    def __repr__(self) -> str:
        return f"LLMModel(name={self.name!r}, max_tokens={self.max_tokens})"

    def __str__(self) -> str:
        return self.describe()

# Creating objects
gpt4 = LLMModel("gpt-4o", 128000)
claude = LLMModel("claude-3-5-sonnet", 200000)

print(gpt4.describe())          # gpt-4o (max_tokens=128000)
print(LLMModel.provider)        # OpenAI (class variable)
print(repr(gpt4))               # LLMModel(name='gpt-4o', max_tokens=128000)

2. Encapsulation

Control access to internal data using naming conventions and properties.

python
class EmbeddingModel:
    def __init__(self, model: str, api_key: str):
        self.model = model              # public
        self._endpoint = "https://api.openai.com"  # protected (convention)
        self.__api_key = api_key        # private (name-mangled to _EmbeddingModel__api_key)

    @property
    def api_key(self) -> str:
        """Getter - read-only access."""
        return "****" + self.__api_key[-4:]

    @api_key.setter
    def api_key(self, value: str):
        """Setter - validate before setting."""
        if not value.startswith("sk-"):
            raise ValueError("Invalid API key format")
        self.__api_key = value

    @staticmethod
    def supported_models() -> list[str]:
        """Static method - no access to self or cls."""
        return ["text-embedding-3-small", "text-embedding-3-large"]

    @classmethod
    def from_env(cls, model: str) -> "EmbeddingModel":
        """Class method - factory pattern using cls."""
        import os
        return cls(model, os.environ.get("OPENAI_API_KEY", ""))

embed = EmbeddingModel("text-embedding-3-small", "sk-abc1234")
print(embed.api_key)            # ****1234 (masked via property)
embed.api_key = "sk-xyz9999"    # setter validates format
Access LevelConventionAccessible From
Public
text
self.name
Anywhere
Protected
text
self._name
Class + subclasses (by convention)
Private
text
self.__name
Only the defining class (name-mangled)

3. Inheritance

A child class inherits all attributes and methods from its parent.

python
class BaseModel:
    """Base class for all AI models."""

    def __init__(self, name: str, version: str):
        self.name = name
        self.version = version

    def info(self) -> str:
        return f"{self.name} v{self.version}"

    def predict(self, prompt: str) -> str:
        raise NotImplementedError("Subclasses must implement predict()")


class ChatModel(BaseModel):
    """Single-turn chat model."""

    def __init__(self, name: str, version: str, temperature: float = 0.7):
        super().__init__(name, version)     # call parent __init__
        self.temperature = temperature

    def predict(self, prompt: str) -> str:
        return f"[{self.name}] Response to: {prompt[:50]}..."


class MultiModalModel(ChatModel):
    """Chat model that also handles images."""

    def __init__(self, name: str, version: str, supports_vision: bool = True):
        super().__init__(name, version)
        self.supports_vision = supports_vision

    def predict_with_image(self, prompt: str, image_path: str) -> str:
        return f"[{self.name}] Analysing image + prompt..."

    def info(self) -> str:
        base = super().info()               # call parent method
        return f"{base} | vision={self.supports_vision}"


gpt4o = MultiModalModel("gpt-4o", "2024-11")
print(gpt4o.info())             # gpt-4o v2024-11 | vision=True
print(gpt4o.predict("Hello"))   # [gpt-4o] Response to: Hello...
print(isinstance(gpt4o, BaseModel))   # True - inheritance chain
print(isinstance(gpt4o, ChatModel))   # True

Multiple Inheritance

python
class Loggable:
    def log(self, msg: str):
        print(f"[LOG] {msg}")

class Cacheable:
    _cache: dict = {}

    def cache(self, key: str, value):
        self._cache[key] = value

    def get_cached(self, key: str):
        return self._cache.get(key)

class SmartModel(BaseModel, Loggable, Cacheable):
    """Inherits from multiple parents."""

    def predict(self, prompt: str) -> str:
        cached = self.get_cached(prompt)
        if cached:
            self.log("Cache hit!")
            return cached
        result = f"[{self.name}] {prompt[:30]}..."
        self.cache(prompt, result)
        self.log(f"Cached result for: {prompt[:20]}")
        return result

model = SmartModel("llama-3", "3.1")
model.predict("What is RAG?")   # computes and caches
model.predict("What is RAG?")   # Cache hit!

MRO (Method Resolution Order): Python uses the C3 linearisation algorithm to determine which parent method gets called first. Use

text
ClassName.__mro__
to inspect.


4. Abstraction

Define a common interface that all subclasses must implement.

python
from abc import ABC, abstractmethod

class VectorStore(ABC):
    """Abstract base class - cannot be instantiated directly."""

    @abstractmethod
    def add_documents(self, docs: list[str]) -> None:
        """Must be implemented by every subclass."""
        ...

    @abstractmethod
    def similarity_search(self, query: str, k: int = 5) -> list[str]:
        ...

    def search_and_print(self, query: str) -> None:
        """Concrete method - shared by all subclasses."""
        results = self.similarity_search(query)
        for i, r in enumerate(results, 1):
            print(f"{i}. {r}")


class ChromaStore(VectorStore):
    def add_documents(self, docs: list[str]) -> None:
        print(f"ChromaDB: adding {len(docs)} docs")

    def similarity_search(self, query: str, k: int = 5) -> list[str]:
        return [f"chroma_result_{i}" for i in range(k)]


class FAISSStore(VectorStore):
    def add_documents(self, docs: list[str]) -> None:
        print(f"FAISS: indexing {len(docs)} docs")

    def similarity_search(self, query: str, k: int = 5) -> list[str]:
        return [f"faiss_result_{i}" for i in range(k)]


# VectorStore()  # TypeError: Can't instantiate abstract class
store = ChromaStore()
store.search_and_print("What is attention?")

5. Polymorphism

The same method name behaves differently depending on the object type.

python
class Tokenizer:
    def tokenize(self, text: str) -> list[str]:
        return text.split()

class BPETokenizer(Tokenizer):
    def tokenize(self, text: str) -> list[str]:
        # BPE-specific logic
        return [text[i:i+3] for i in range(0, len(text), 3)]

class WordPieceTokenizer(Tokenizer):
    def tokenize(self, text: str) -> list[str]:
        # WordPiece-specific logic
        return ["##" + w if i > 0 else w for i, w in enumerate(text.split())]


def process_text(tokenizer: Tokenizer, text: str) -> list[str]:
    """Works with ANY tokenizer - polymorphism in action."""
    return tokenizer.tokenize(text)

tokenizers = [Tokenizer(), BPETokenizer(), WordPieceTokenizer()]
for t in tokenizers:
    print(type(t).__name__, "->", process_text(t, "hello world"))

Duck Typing

Python doesn't require explicit inheritance - if an object has the right methods, it works.

python
class LocalLLM:
    def generate(self, prompt: str) -> str:
        return f"[Local] {prompt}"

class APIModel:
    def generate(self, prompt: str) -> str:
        return f"[API] {prompt}"

def run_pipeline(model, prompt: str):
    # No isinstance check needed - just needs a .generate() method
    return model.generate(prompt)

run_pipeline(LocalLLM(), "What is LoRA?")   # works
run_pipeline(APIModel(), "What is LoRA?")   # works

6. Magic (Dunder) Methods

Special methods that define how objects behave with Python operators.

python
class TokenBudget:
    def __init__(self, limit: int):
        self.limit = limit
        self.used = 0

    def __add__(self, tokens: int) -> "TokenBudget":
        new = TokenBudget(self.limit)
        new.used = self.used + tokens
        return new

    def __len__(self) -> int:
        return self.used

    def __bool__(self) -> bool:
        return self.used < self.limit

    def __repr__(self) -> str:
        return f"TokenBudget(used={self.used}/{self.limit})"

    def __eq__(self, other: "TokenBudget") -> bool:
        return self.used == other.used and self.limit == other.limit

    def __lt__(self, other: "TokenBudget") -> bool:
        return self.used < other.used

budget = TokenBudget(4096)
budget = budget + 1200
print(len(budget))          # 1200
print(bool(budget))         # True (budget not exceeded)
print(repr(budget))         # TokenBudget(used=1200/4096)
Dunder MethodTriggered By
text
__init__
text
MyClass()
text
__repr__
text
repr(obj)
text
__str__
text
str(obj)
,
text
print(obj)
text
__len__
text
len(obj)
text
__bool__
text
if obj:
text
__add__
text
obj + other
text
__eq__
text
obj == other
text
__lt__
text
obj < other
text
__getitem__
text
obj[key]
text
__iter__
text
for x in obj:
text
__enter__
/
text
__exit__
text
with obj:

7. Class vs Static vs Instance Methods

python
class ModelRegistry:
    _registry: dict = {}
    _instance_count = 0

    def __init__(self, model_name: str):
        self.model_name = model_name
        ModelRegistry._instance_count += 1

    def describe(self) -> str:
        """Instance method - accesses self."""
        return f"Model: {self.model_name}"

    @classmethod
    def register(cls, name: str, config: dict) -> None:
        """Class method - accesses cls, used as factory/registry."""
        cls._registry[name] = config

    @classmethod
    def get_instance_count(cls) -> int:
        return cls._instance_count

    @staticmethod
    def validate_name(name: str) -> bool:
        """Static method - no access to self or cls."""
        return name.isidentifier() and len(name) > 0

ModelRegistry.register("gpt-4o", {"provider": "openai", "tokens": 128000})
print(ModelRegistry.validate_name("gpt-4o"))     # False (hyphens not allowed)
print(ModelRegistry.validate_name("gpt4o"))      # True
m = ModelRegistry("llama-3")
print(ModelRegistry.get_instance_count())        # 1

8. Dataclasses (Modern OOP)

python
from dataclasses import dataclass, field

@dataclass
class ModelConfig:
    name: str
    temperature: float = 0.7
    max_tokens: int = 4096
    tags: list[str] = field(default_factory=list)

    def is_creative(self) -> bool:
        return self.temperature > 0.8

config = ModelConfig(name="gpt-4o", temperature=0.9, tags=["chat", "vision"])
print(config)               # ModelConfig(name='gpt-4o', temperature=0.9, ...)
print(config.is_creative()) # True

Pro tip: Pydantic's

text
BaseModel
is a supercharged dataclass - used extensively in LangChain and FastAPI for validation and serialisation.


OOP in Gen AI - Real-World Pattern

python
from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class Message:
    role: str
    content: str

class BaseLLM(ABC):
    def __init__(self, model: str, temperature: float = 0.7):
        self.model = model
        self.temperature = temperature
        self._history: list[Message] = []

    @abstractmethod
    def _call_api(self, messages: list[Message]) -> str:
        ...

    def chat(self, user_input: str) -> str:
        self._history.append(Message("user", user_input))
        response = self._call_api(self._history)
        self._history.append(Message("assistant", response))
        return response

    def clear_history(self):
        self._history.clear()

class OpenAIChat(BaseLLM):
    def _call_api(self, messages: list[Message]) -> str:
        # Real implementation would call openai.chat.completions.create(...)
        return f"[OpenAI/{self.model}] response"

class AnthropicChat(BaseLLM):
    def _call_api(self, messages: list[Message]) -> str:
        # Real implementation would call anthropic.messages.create(...)
        return f"[Anthropic/{self.model}] response"

llm = OpenAIChat("gpt-4o")
print(llm.chat("What is RAG?"))
print(llm.chat("Give an example."))

Quick Reference

ConceptKey Point
ClassBlueprint with
text
__init__
, methods, class vars
Encapsulation
text
_protected
,
text
__private
,
text
@property
Inheritance
text
class Child(Parent):
,
text
super()
Abstraction
text
ABC
+
text
@abstractmethod
PolymorphismOverride methods, duck typing
Dunder methods
text
__repr__
,
text
__str__
,
text
__len__
,
text
__add__
Static method
text
@staticmethod
- no
text
self
or
text
cls
Class method
text
@classmethod
- receives
text
cls
Dataclass
text
@dataclass
- auto-generates boilerplate

Remember: OOP in Python is the backbone of every major Gen AI library. LangChain's

text
BaseLLM
, PyTorch's
text
nn.Module
, and Hugging Face's
text
PreTrainedModel
all use these exact patterns.

Learn more at Python OOP Docs and Real Python OOP Guide.