Concept #161Mediumpython-for-gen-aiimportant

Explain me detailly about the types of principles in Python?

#python#principles#solid#dry#kiss#yagni#zen#pep8#design-patterns

Answer

Principles in Python

Python development is guided by several layers of principles - from language philosophy to software design and architecture. Mastering these makes your Gen AI code cleaner, more maintainable, and easier to scale.


1. The Zen of Python (PEP 20)

The Zen of Python is the guiding philosophy of the language itself. Run

text
import this
in any Python interpreter to see it.

python
import this

Key Tenets (Most Important)

PrincipleMeaning
Beautiful is better than uglyPrioritise readable, elegant code
Explicit is better than implicitBe clear about what your code does
Simple is better than complexAvoid unnecessary complexity
Flat is better than nestedAvoid deep indentation/nesting
Readability countsCode is read more than it is written
Errors should never pass silentlyAlways handle exceptions explicitly
There should be one obvious way to do itPython favours one idiomatic solution
python
# Implicit (avoid)
def get_data(x):
    return x and x[0] or None

# Explicit (preferred)
def get_data(items: list) -> str | None:
    return items[0] if items else None

2. SOLID Principles

SOLID is a set of 5 object-oriented design principles for writing robust, maintainable code.

S - Single Responsibility Principle (SRP)

A class should have only ONE reason to change.

python
# Bad - one class does too much
class LLMPipeline:
    def load_data(self): ...
    def chunk_text(self): ...
    def embed(self): ...
    def store_vectors(self): ...
    def query(self): ...
    def log_results(self): ...

# Good - each class has one responsibility
class DataLoader:
    def load(self, path: str) -> list[str]: ...

class TextChunker:
    def chunk(self, text: str, size: int) -> list[str]: ...

class Embedder:
    def embed(self, chunks: list[str]) -> list[list[float]]: ...

class VectorStore:
    def store(self, embeddings: list[list[float]]) -> None: ...

O - Open/Closed Principle (OCP)

Classes should be open for extension but closed for modification.

python
from abc import ABC, abstractmethod

# Base class - closed for modification
class BaseEmbedder(ABC):
    @abstractmethod
    def embed(self, text: str) -> list[float]:
        ...

# Extend by adding new classes - no modification needed
class OpenAIEmbedder(BaseEmbedder):
    def embed(self, text: str) -> list[float]:
        # calls OpenAI API
        return [0.1, 0.2, 0.3]

class HuggingFaceEmbedder(BaseEmbedder):
    def embed(self, text: str) -> list[float]:
        # uses local HuggingFace model
        return [0.4, 0.5, 0.6]

class CohereEmbedder(BaseEmbedder):
    def embed(self, text: str) -> list[float]:
        # calls Cohere API
        return [0.7, 0.8, 0.9]

# Pipeline works with any embedder without changing its code
def build_index(embedder: BaseEmbedder, texts: list[str]):
    return [embedder.embed(t) for t in texts]

L - Liskov Substitution Principle (LSP)

A subclass should be usable wherever its parent class is expected, without breaking the program.

python
class LLM:
    def generate(self, prompt: str, max_tokens: int = 512) -> str:
        raise NotImplementedError

class GPT4(LLM):
    def generate(self, prompt: str, max_tokens: int = 512) -> str:
        return f"[GPT-4] {prompt[:30]}..."  # same interface, different impl

class Claude(LLM):
    def generate(self, prompt: str, max_tokens: int = 512) -> str:
        return f"[Claude] {prompt[:30]}..."

# Any LLM subclass can be substituted here
def run_chain(llm: LLM, prompt: str) -> str:
    return llm.generate(prompt)

run_chain(GPT4(), "Explain RAG")    # works
run_chain(Claude(), "Explain RAG")  # works identically

I - Interface Segregation Principle (ISP)

Don't force classes to implement methods they don't need. Prefer small, focused interfaces.

python
from abc import ABC, abstractmethod

# Bad - one fat interface
class AIModel(ABC):
    @abstractmethod
    def generate_text(self): ...
    @abstractmethod
    def generate_image(self): ...
    @abstractmethod
    def transcribe_audio(self): ...
    @abstractmethod
    def embed_text(self): ...

# Good - small, focused interfaces
class TextGenerator(ABC):
    @abstractmethod
    def generate_text(self, prompt: str) -> str: ...

class ImageGenerator(ABC):
    @abstractmethod
    def generate_image(self, prompt: str) -> bytes: ...

class TextEmbedder(ABC):
    @abstractmethod
    def embed_text(self, text: str) -> list[float]: ...

# Classes only implement what they need
class GPT4(TextGenerator):
    def generate_text(self, prompt: str) -> str:
        return f"[GPT-4] {prompt}"

class DALL_E(ImageGenerator):
    def generate_image(self, prompt: str) -> bytes:
        return b"image_bytes"

class AdaEmbedder(TextEmbedder):
    def embed_text(self, text: str) -> list[float]:
        return [0.1, 0.2, 0.3]

D - Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

python
from abc import ABC, abstractmethod

class VectorDB(ABC):
    @abstractmethod
    def search(self, query_vector: list[float], k: int) -> list[str]: ...

# Low-level modules
class ChromaDB(VectorDB):
    def search(self, query_vector: list[float], k: int) -> list[str]:
        return [f"chroma_result_{i}" for i in range(k)]

class Pinecone(VectorDB):
    def search(self, query_vector: list[float], k: int) -> list[str]:
        return [f"pinecone_result_{i}" for i in range(k)]

# High-level module depends on abstraction, not concrete class
class RAGPipeline:
    def __init__(self, db: VectorDB):   # injected dependency
        self.db = db

    def answer(self, query: str) -> str:
        results = self.db.search([0.1, 0.2], k=3)
        return f"Answer based on: {results}"

# Swap implementations without changing RAGPipeline
rag = RAGPipeline(db=ChromaDB())
rag2 = RAGPipeline(db=Pinecone())

3. DRY — Don't Repeat Yourself

Every piece of knowledge should have a single, authoritative representation in the codebase.

python
# Bad - duplicated logic
def process_openai_response(response):
    text = response.strip()
    text = text.replace("\n\n", "\n")
    return text[:2000]

def process_anthropic_response(response):
    text = response.strip()
    text = text.replace("\n\n", "\n")
    return text[:2000]

# Good - single source of truth
def clean_response(response: str, max_len: int = 2000) -> str:
    return response.strip().replace("\n\n", "\n")[:max_len]

openai_result = clean_response(openai_raw)
anthropic_result = clean_response(anthropic_raw)

4. KISS — Keep It Simple, Stupid

Prefer the simplest solution that works. Avoid over-engineering.

python
# Over-engineered
class PromptBuilderFactoryManagerSingleton:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    def build_prompt(self, template, **kwargs):
        return template.format(**kwargs)

# Simple and sufficient
def build_prompt(template: str, **kwargs) -> str:
    return template.format(**kwargs)

prompt = build_prompt("Answer this: {question}", question="What is RAG?")

5. YAGNI — You Aren't Gonna Need It

Don't add functionality until it is actually needed.

python
# Bad - building for hypothetical future
class LLMClient:
    def __init__(self):
        self.cache = {}
        self.retry_count = 3
        self.fallback_models = []
        self.cost_tracker = CostTracker()
        self.ab_test_router = ABTestRouter()
        self.analytics = AnalyticsDashboard()
        # ... none of these are needed yet

# Good - start minimal, add as needed
class LLMClient:
    def __init__(self, model: str, api_key: str):
        self.model = model
        self.api_key = api_key

    def generate(self, prompt: str) -> str:
        # call API and return response
        ...

6. Separation of Concerns (SoC)

Different aspects of a program should be handled by separate, independent components.

python
# Concerns mixed (bad)
def handle_request(user_query: str):
    # data retrieval
    docs = fetch_from_db(user_query)
    # business logic
    context = "\n".join(docs[:3])
    prompt = f"Answer using context:\n{context}\n\nQuestion: {user_query}"
    # LLM call
    response = openai.chat(prompt)
    # formatting
    return f"<b>{response}</b>"

# Concerns separated (good)
class Retriever:
    def fetch(self, query: str) -> list[str]: ...

class PromptBuilder:
    def build(self, query: str, docs: list[str]) -> str: ...

class LLMClient:
    def generate(self, prompt: str) -> str: ...

class ResponseFormatter:
    def format(self, response: str) -> str: ...

# Orchestrator composes the concerns
class RAGPipeline:
    def __init__(self, retriever, prompt_builder, llm, formatter):
        self.retriever = retriever
        self.prompt_builder = prompt_builder
        self.llm = llm
        self.formatter = formatter

    def run(self, query: str) -> str:
        docs = self.retriever.fetch(query)
        prompt = self.prompt_builder.build(query, docs)
        response = self.llm.generate(prompt)
        return self.formatter.format(response)

7. Composition Over Inheritance

Prefer composing objects from smaller pieces over building deep inheritance hierarchies.

python
# Deep inheritance (fragile)
class Animal: ...
class Mammal(Animal): ...
class Pet(Mammal): ...
class Dog(Pet): ...
class ServiceDog(Dog): ...  # inheritance chain becomes unmanageable

# Composition (flexible)
class Retriever:
    def retrieve(self, query: str) -> list[str]: ...

class Reranker:
    def rerank(self, docs: list[str], query: str) -> list[str]: ...

class Generator:
    def generate(self, prompt: str) -> str: ...

# Compose behaviours at runtime
class AdvancedRAG:
    def __init__(self, retriever: Retriever, reranker: Reranker, generator: Generator):
        self.retriever = retriever
        self.reranker = reranker
        self.generator = generator

8. Pythonic Principles (PEP 8 Highlights)

python
# Naming conventions
variable_name = "snake_case"         # variables and functions
CONSTANT_VALUE = 42                  # constants
ClassName = "PascalCase"             # classes
_private = "single underscore"       # internal use

# Prefer list comprehensions over map/filter for simple cases
squares = [x**2 for x in range(10)]

# Use context managers for resources
with open("file.txt") as f:
    data = f.read()

# Unpack tuples meaningfully
first, *rest = [1, 2, 3, 4]

# Use enumerate instead of range(len(...))
for i, item in enumerate(["a", "b", "c"]):
    print(i, item)

# Use f-strings over .format() or %
name = "RAG"
print(f"Technique: {name}")          # preferred over "Technique: %s" % name

# Return early to reduce nesting
def process(data):
    if not data:
        return None                  # early return
    if not isinstance(data, list):
        return None
    return [item.strip() for item in data]

Summary Table

PrincipleCore IdeaApplied To
Zen of PythonSimple, explicit, readableLanguage philosophy
SRPOne class, one jobClass design
OCPExtend, don't modifyClass design
LSPSubclasses are substitutableInheritance
ISPSmall focused interfacesInterface design
DIPDepend on abstractionsModule coupling
DRYNo duplicate logicCode organisation
KISSSimplest solution winsAll code
YAGNIBuild only what's neededFeature design
SoCSeparate concernsArchitecture
Composition > InheritanceCompose over inheritObject design

Pro tip: In Gen AI engineering, these principles are especially critical - LangChain, LlamaIndex, and PyTorch are all built around SOLID and SoC. Understanding them helps you extend these frameworks without breaking existing functionality.

Learn more at PEP 20 - The Zen of Python, PEP 8 Style Guide, and Real Python SOLID Guide.