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 = "gpt-4o"
                      
                          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,
                                      messages=[
                                          {"role": "system", "content": system or self._default_system},
                                          {"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,
                                      messages=[
                                          {"role": "system", "content": system or self._default_system},
                                          *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

Not every method needs an instance. A @classmethod receives the class itself (cls) rather than an instance — the idiomatic way to write alternative constructors, the same role a C# static factory method plays. A @staticmethod belongs to the class purely for namespacing and takes no self or cls at all. The registry below uses both to look up and construct model clients by name.

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("gpt-4o"))  # 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