AI Systemsintermediate
Classes and Objects
Build Python classes for AI engineering: instance attributes, class attributes, methods, properties, encapsulation, and real-world patterns from LangChain and ML codebases.
Asma Hafeez KhanMay 16, 20265 min read
PythonClassesOOPObjectsInstance MethodsProperties
Why Classes?
A class groups related data and behavior together. Instead of passing five separate variables to every function, you bundle them into an object:
Python
# Without class: unwieldy
def calculate_dose(drug_name, base_dose, weight, renal_factor, age_factor):
return base_dose * weight * renal_factor * age_factor
# With class: organized, self-documenting
class DoseCalculator:
def __init__(self, drug_name: str, base_dose_per_kg: float):
self.drug_name = drug_name
self.base_dose_per_kg = base_dose_per_kg
def calculate(self, weight_kg: float, renal_factor: float = 1.0, age_factor: float = 1.0) -> float:
return self.base_dose_per_kg * weight_kg * renal_factor * age_factorDefining a Class
Python
class Drug:
"""Represents a medication with its clinical properties."""
# Class attribute: shared by all instances
reference_database = "Lexicomp"
def __init__(self, name: str, category: str, dose_mg: float):
# Instance attributes: unique to each object
self.name = name
self.category = category
self.dose_mg = dose_mg
self._interactions: list[str] = [] # Private by convention (underscore)
def add_interaction(self, other_drug: str) -> None:
"""Add a known interacting drug."""
self._interactions.append(other_drug)
def has_interaction_with(self, other_drug: str) -> bool:
"""Check if this drug interacts with another."""
return other_drug.lower() in [d.lower() for d in self._interactions]
def __str__(self) -> str:
"""Human-readable string representation."""
return f"{self.name} ({self.category}, {self.dose_mg}mg)"
def __repr__(self) -> str:
"""Developer-facing representation — useful in debuggers."""
return f"Drug(name={self.name!r}, category={self.category!r}, dose_mg={self.dose_mg})"
# Creating instances
warfarin = Drug(name="warfarin", category="anticoagulant", dose_mg=5.0)
aspirin = Drug(name="aspirin", category="nsaid", dose_mg=81.0)
# Accessing attributes
print(warfarin.name) # "warfarin"
print(warfarin.dose_mg) # 5.0
print(Drug.reference_database) # "Lexicomp" — class attribute via class
print(warfarin.reference_database) # "Lexicomp" — also accessible via instance
# Calling methods
warfarin.add_interaction("aspirin")
print(warfarin.has_interaction_with("aspirin")) # True
print(warfarin.has_interaction_with("metformin")) # False
# __str__ called by print() and str()
print(warfarin) # "warfarin (anticoagulant, 5.0mg)"Instance vs Class Attributes
Python
class LLMConfig:
# Class attribute: shared default
default_model = "gpt-4o"
call_count = 0 # Shared counter across all instances — careful!
def __init__(self, model: str | None = None, temperature: float = 0.0):
# Instance attribute: per-object
self.model = model or LLMConfig.default_model
self.temperature = temperature
def invoke(self, prompt: str) -> str:
LLMConfig.call_count += 1 # Mutate class attribute via class name
return f"Response from {self.model}"
config_a = LLMConfig()
config_b = LLMConfig(model="claude-sonnet-4-6", temperature=0.3)
print(config_a.model) # "gpt-4o"
print(config_b.model) # "claude-sonnet-4-6"
config_a.invoke("Hello")
config_b.invoke("Hello")
print(LLMConfig.call_count) # 2 — shared across all instancesProperties: Computed and Validated Attributes
Python
class PatientRecord:
def __init__(self, patient_id: str, weight_kg: float, height_cm: float):
self.patient_id = patient_id
self._weight_kg = weight_kg
self._height_cm = height_cm
@property
def weight_kg(self) -> float:
"""Weight in kilograms."""
return self._weight_kg
@weight_kg.setter
def weight_kg(self, value: float) -> None:
if value <= 0 or value > 500:
raise ValueError(f"Invalid weight: {value}kg")
self._weight_kg = value
@property
def bmi(self) -> float:
"""Body mass index — computed from weight and height."""
height_m = self._height_cm / 100
return round(self._weight_kg / (height_m ** 2), 1)
@property
def bmi_category(self) -> str:
if self.bmi < 18.5:
return "underweight"
elif self.bmi < 25:
return "normal"
elif self.bmi < 30:
return "overweight"
return "obese"
patient = PatientRecord("P001", weight_kg=80.0, height_cm=175.0)
print(patient.bmi) # 26.1 — computed, no attribute stored
print(patient.bmi_category) # "overweight"
patient.weight_kg = 75.0 # Calls the setter — validated
patient.weight_kg = -10 # Raises ValueError
patient.bmi = 25.0 # AttributeError: bmi has no setter (read-only)Class Methods and Static Methods
Python
class Drug:
_registry: dict[str, "Drug"] = {} # Class-level drug registry
def __init__(self, name: str, category: str):
self.name = name
self.category = category
Drug._registry[name.lower()] = self
@classmethod
def from_dict(cls, data: dict) -> "Drug":
"""Alternative constructor — create Drug from a dict."""
return cls(name=data["name"], category=data["category"])
@classmethod
def lookup(cls, name: str) -> "Drug | None":
"""Look up a drug by name."""
return cls._registry.get(name.lower())
@staticmethod
def normalize_name(name: str) -> str:
"""Normalize a drug name — no access to class or instance."""
return name.lower().strip()
# Class method as alternative constructor
warfarin = Drug.from_dict({"name": "warfarin", "category": "anticoagulant"})
aspirin = Drug.from_dict({"name": "aspirin", "category": "nsaid"})
# Class method as factory
found = Drug.lookup("warfarin")
print(found.category) # "anticoagulant"
# Static method — utility function
print(Drug.normalize_name(" WARFARIN ")) # "warfarin"Real Pattern: RAG Session Manager
Python
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_core.messages import HumanMessage, AIMessage
class ClinicalRAGSession:
"""A per-session RAG chatbot with isolated history."""
_model = ChatOpenAI(model="gpt-4o", temperature=0) # Shared across sessions
_embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
def __init__(self, session_id: str, user_id: str, max_history: int = 10):
self.session_id = session_id
self.user_id = user_id
self._max_history = max_history
self._history: list = []
self._vectorstore = Chroma(
collection_name="clinical_knowledge",
embedding_function=self._embeddings,
persist_directory="./chroma_db",
)
@property
def history_length(self) -> int:
return len(self._history)
def chat(self, question: str) -> str:
"""Process a question and return an answer."""
# Retrieve
docs = self._vectorstore.similarity_search(question, k=3)
context = "\n\n".join(d.page_content for d in docs)
# Build messages
messages = [
{"role": "system", "content": f"Answer using this context:\n{context}"},
*self._history[-self._max_history:],
{"role": "user", "content": question},
]
# Generate
response = self._model.invoke(messages)
answer = response.content
# Update history
self._history.extend([
{"role": "user", "content": question},
{"role": "assistant", "content": answer},
])
return answer
def reset(self) -> None:
self._history.clear()
def __repr__(self) -> str:
return f"ClinicalRAGSession(session_id={self.session_id!r}, turns={self.history_length})"
session = ClinicalRAGSession(session_id="sess_001", user_id="pharmacist_1")
answer1 = session.chat("What is warfarin?")
answer2 = session.chat("What are its interactions?") # History-aware
print(repr(session)) # ClinicalRAGSession(session_id='sess_001', turns=2)Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.