Explain me detailly about the types of principles in Python?
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
import thispythonimport this
Key Tenets (Most Important)
| Principle | Meaning |
|---|---|
| Beautiful is better than ugly | Prioritise readable, elegant code |
| Explicit is better than implicit | Be clear about what your code does |
| Simple is better than complex | Avoid unnecessary complexity |
| Flat is better than nested | Avoid deep indentation/nesting |
| Readability counts | Code is read more than it is written |
| Errors should never pass silently | Always handle exceptions explicitly |
| There should be one obvious way to do it | Python 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.
pythonfrom 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.
pythonclass 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.
pythonfrom 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.
pythonfrom 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
| Principle | Core Idea | Applied To |
|---|---|---|
| Zen of Python | Simple, explicit, readable | Language philosophy |
| SRP | One class, one job | Class design |
| OCP | Extend, don't modify | Class design |
| LSP | Subclasses are substitutable | Inheritance |
| ISP | Small focused interfaces | Interface design |
| DIP | Depend on abstractions | Module coupling |
| DRY | No duplicate logic | Code organisation |
| KISS | Simplest solution wins | All code |
| YAGNI | Build only what's needed | Feature design |
| SoC | Separate concerns | Architecture |
| Composition > Inheritance | Compose over inherit | Object 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.