TechWayFit
● Complete Learning Series Python & AI Engineering for .NET Developers

Object-Oriented Python for AI Components

Tech Buddy June 24, 2026 3 min read
Object-Oriented Python for AI Components

You know OOP. You've written C# classes, interfaces, abstract base classes, and inheritance hierarchies. Python's OOP covers the same ground, but with important syntactic and philosophical differences. The biggest one: Python makes encapsulation a convention rather than an enforcement — there's no private keyword that the runtime enforces. The second biggest: Python's "duck typing" means you often don't need explicit interfaces.

This lesson builds a realistic AI client abstraction — the kind of wrapper class you'd write in your first week on a real AI project — to show how Python OOP maps to C# patterns.

Defining a Class

C# Class
public class AIClient
                      {
                          private readonly string _apiKey;
                          private readonly string _model;
                          private int _requestCount;
                      
                          public AIClient(string apiKey, string model)
                          {
                              _apiKey = apiKey;
                              _model = model;
                              _requestCount = 0;
                          }
                      
                          public int RequestCount => _requestCount;
                      }
Python Class
class AIClient:
                          def __init__(self, api_key: str, model: str):
                              # _prefix = convention for "private"
                              self._api_key = api_key
                              self._model = model
                              self._request_count = 0
                      
                          @property
                          def request_count(self) -> int:
                              return self._request_count
                      
                          # All methods take 'self' as first arg
                          # No access modifiers — convention only

Key OOP Differences from C#

  • self is explicit — every instance method takes self as its first parameter. Python doesn't hide it the way C# hides this.
  • No access modifiers — prefix with _ for "internal" convention, __ for name-mangled (hard to access externally but not truly private).
  • @property instead of getters/setters — Python uses decorators to make methods look like attributes.
  • __init__ not a constructor keyword — it's just the initializer method that runs after the object is created.

Building a Full AI Client Abstraction

Here's the kind of wrapper class you'd build to centralize all your AI API interactions, add observability, and make testing easier:

from __future__ import annotations
                      import time
                      import logging
                      from typing import Optional
                      from dataclasses import dataclass, field
                      
                      from openai import OpenAI
                      
                      logger = logging.getLogger(__name__)
                      
                      
                      @dataclass
                      class RequestRecord:
                          """Tracks metadata for a single API request."""
                          prompt_length: int
                          response_length: int
                          input_tokens: int
                          output_tokens: int
                          latency_ms: float
                          model: str
                          success: bool
                      
                      
                      class OpenAIClient:
                          """
                          A production-ready wrapper around the OpenAI SDK.
                      
                          Adds:
                          - Request tracking and metrics
                          - Configurable defaults
                          - Consistent error handling
                          - Usage reporting
                          """
                      
                          DEFAULT_MODEL = "claude-3-5-sonnet-20241022"
                      
                          def __init__(
                              self,
                              api_key: str,
                              model: str = DEFAULT_MODEL,
                              max_tokens: int = 1024,
                              default_system: str = "You are a helpful AI assistant.",
                          ) -> None:
                              self._client = OpenAI(api_key=api_key)
                              self._model = model
                              self._max_tokens = max_tokens
                              self._default_system = default_system
                              self._history: list[RequestRecord] = []
                      
                          # --- Properties (read-only computed attributes) ---
                      
                          @property
                          def model(self) -> str:
                              return self._model
                      
                          @property
                          def total_requests(self) -> int:
                              return len(self._history)
                      
                          @property
                          def total_tokens_used(self) -> int:
                              return sum(r.input_tokens + r.output_tokens for r in self._history)
                      
                          @property
                          def success_rate(self) -> float:
                              if not self._history:
                                  return 0.0
                              return sum(1 for r in self._history if r.success) / len(self._history)
                      
                          # --- Public methods ---
                      
                          def complete(
                              self,
                              prompt: str,
                              system: Optional[str] = None,
                              max_tokens: Optional[int] = None,
                          ) -> str:
                              """
                              Send a single-turn completion request.
                              Returns the text content of the response.
                              """
                              start = time.monotonic()
                              success = False
                              response = None
                      
                              try:
                                  response = self._client.chat.completions.create(
                                      model=self._model,
                                      max_tokens=max_tokens or self._max_tokens,
                                      system=system or self._default_system,
                                      messages=[{"role": "user", "content": prompt}],
                                  )
                                  success = True
                                  return response.choices[0].message.content
                      
                              except openai.APIError as e:
                                  logger.error(f"API error in complete(): {e}")
                                  raise
                      
                              finally:
                                  latency = (time.monotonic() - start) * 1000
                                  self._record_request(
                                      prompt=prompt,
                                      response=response,
                                      latency_ms=latency,
                                      success=success,
                                  )
                      
                          def chat(
                              self,
                              messages: list[dict],
                              system: Optional[str] = None,
                          ) -> str:
                              """Send a multi-turn messages request."""
                              start = time.monotonic()
                              success = False
                              response = None
                      
                              try:
                                  response = self._client.chat.completions.create(
                                      model=self._model,
                                      max_tokens=self._max_tokens,
                                      system=system or self._default_system,
                                      messages=messages,
                                  )
                                  success = True
                                  return response.choices[0].message.content
                      
                              except openai.APIError as e:
                                  logger.error(f"API error in chat(): {e}")
                                  raise
                      
                              finally:
                                  latency = (time.monotonic() - start) * 1000
                                  prompt_text = messages[-1].get("content", "") if messages else ""
                                  self._record_request(
                                      prompt=str(prompt_text),
                                      response=response,
                                      latency_ms=latency,
                                      success=success,
                                  )
                      
                          def usage_report(self) -> dict:
                              """Return a summary of API usage for this client instance."""
                              return {
                                  "total_requests": self.total_requests,
                                  "total_tokens": self.total_tokens_used,
                                  "success_rate": f"{self.success_rate:.1%}",
                                  "avg_latency_ms": (
                                      sum(r.latency_ms for r in self._history) / len(self._history)
                                      if self._history else 0.0
                                  ),
                              }
                      
                          # --- Private methods ---
                      
                          def _record_request(
                              self,
                              prompt: str,
                              response: Optional[openai.types.chat.ChatCompletion],
                              latency_ms: float,
                              success: bool,
                          ) -> None:
                              """Internal method — records a request in the history."""
                              record = RequestRecord(
                                  prompt_length=len(prompt),
                                  response_length=len(response.choices[0].message.content) if response else 0,
                                  input_tokens=response.usage.prompt_tokens if response else 0,
                                  output_tokens=response.usage.completion_tokens if response else 0,
                                  latency_ms=latency_ms,
                                  model=self._model,
                                  success=success,
                              )
                              self._history.append(record)
                              logger.debug(f"Request completed: {latency_ms:.0f}ms, {record.input_tokens + record.output_tokens} tokens")

Inheritance and Abstract Base Classes

Python inheritance uses parentheses instead of colons. Abstract base classes use the abc module — the equivalent of a C# interface or abstract class:

from abc import ABC, abstractmethod
                      
                      class BaseLLMClient(ABC):
                          """Abstract interface for LLM clients — swap providers without changing app code."""
                      
                          @abstractmethod
                          def complete(self, prompt: str, **kwargs) -> str:
                              """Send a completion request and return the response text."""
                              ...
                      
                          @abstractmethod
                          def usage_report(self) -> dict:
                              """Return a usage summary."""
                              ...
                      
                          def test_connection(self) -> bool:
                              """Concrete method shared by all subclasses."""
                              try:
                                  result = self.complete("Say 'ok'.", max_tokens=10)
                                  return bool(result)
                              except Exception:
                                  return False
                      
                      
                      class OpenAIClient(BaseLLMClient):
                          """OpenAI implementation of BaseLLMClient."""
                      
                          def __init__(self, api_key: str, model: str) -> None:
                              from openai import OpenAI
                              self._client = OpenAI(api_key=api_key)
                              self._model = model
                              self._calls = 0
                      
                          def complete(self, prompt: str, **kwargs) -> str:
                              self._calls += 1
                              response = self._client.chat.completions.create(
                                  model=self._model,
                                  max_tokens=kwargs.get("max_tokens", 1024),
                                  messages=[{"role": "user", "content": prompt}],
                              )
                              return response.choices[0].message.content
                      
                          def usage_report(self) -> dict:
                              return {"provider": "openai", "total_calls": self._calls}
                      
                      
                      class OpenAIClient(BaseLLMClient):
                          """OpenAI implementation — same interface, different provider."""
                      
                          def complete(self, prompt: str, **kwargs) -> str:
                              ...  # OpenAI implementation
                      
                          def usage_report(self) -> dict:
                              return {"provider": "openai", "total_calls": 0}
                      
                      
                      # The application code doesn't need to know which provider it's using:
                      def generate_summary(client: BaseLLMClient, text: str) -> str:
                          return client.complete(f"Summarize in 3 sentences:\n\n{text}")

Class Methods and Static Methods

class ModelRegistry:
                          """Manages known model configurations."""
                      
                          _registry: dict[str, dict] = {}  # class-level attribute (like C# static field)
                      
                          @classmethod
                          def register(cls, name: str, config: dict) -> None:
                              """Class method — receives the class, not the instance."""
                              cls._registry[name] = config
                      
                          @staticmethod
                          def validate_model_name(name: str) -> bool:
                              """Static method — no class or instance reference needed."""
                              return name.startswith("claude-") or name.startswith("gpt-")
                      
                          @classmethod
                          def from_config_file(cls, path: str) -> "ModelRegistry":
                              """Factory constructor — common pattern for multiple constructors."""
                              import json
                              with open(path) as f:
                                  configs = json.load(f)
                              registry = cls()
                              for name, config in configs.items():
                                  registry.register(name, config)
                              return registry
                      
                      
                      # Usage
                      ModelRegistry.register("gpt-4o", {"max_tokens": 8192, "context_window": 200_000})
                      print(ModelRegistry.validate_model_name("claude-3-5-sonnet-20241022"))  # True

Key Takeaways

  • All Python instance methods take self explicitly — it's how Python connects a method call to its object
  • Use single underscore (_attr) to signal "private by convention" — Python has no runtime privacy enforcement
  • @property turns a method into an attribute-style accessor — the equivalent of a C# getter property
  • Use ABC + @abstractmethod to define interfaces — subclasses that don't implement abstract methods raise TypeError at instantiation
  • @classmethod receives the class as first arg (useful for factory constructors); @staticmethod receives nothing (pure utility functions on the class namespace)
  • Python's duck typing means you often don't need inheritance — any object with the right methods works, regardless of its class hierarchy

Comments

Loading comments…

Leave a comment