Python Essentials for AI Engineer

Course Content

Python Essentials for AI Engineer

6 sections · 48 lessons

What is Inheritance?


One base class, one method to fill inLLMClient:retry, timingProviderClient:real _sendFakeClient:echo _send
Children write only _send and get retries and timing for free — the same deal nn.Module offers when you write only forward.

What you need to know

Syntax, overriding and super()

Python
class Model:    def __init__(self, name):        self.name = name    def predict(self, x):        raise NotImplementedError    def describe(self):        return f"{type(self).__name__}({self.name})"class ThresholdClassifier(Model):    def __init__(self, name, threshold=0.5):        super().__init__(name)                # run the parent's setup        self.threshold = threshold    def predict(self, x):                     # override        return "fraud" if x >= self.threshold else "ok"clf = ThresholdClassifier("fraud-v1", 0.8)print(clf.describe(), clf.predict(0.93))      # ThresholdClassifier(fraud-v1) fraudprint(isinstance(clf, Model))                 # True -> a child IS-A parent

describe is inherited unchanged; predict is replaced. isinstance returns True for parents too, so any code written for Model accepts a ThresholdClassifier.

Abstract base classes

raise NotImplementedError only fails when the method is called. An abstract base class fails earlier, when someone tries to create an incomplete object:

Python
from abc import ABC, abstractmethodclass BaseModel(ABC):    @abstractmethod    def predict(self, x): ...class Incomplete(BaseModel):    passtry:    Incomplete()except TypeError as e:    print(type(e).__name__)      # TypeError -> can't instantiate without predict()

Multiple inheritance and the MRO

A class can have several parents: class C(A, B). Python flattens the family tree into one ordered list, the MRO, visible as C.__mro__, and searches it left to right. super() means "the next class in the MRO", which is not always the direct parent.

Composition over inheritance

If a Chatbot uses a retriever, it should hold one (self.retriever = retriever), not inherit from Retriever. Composition lets you swap parts at runtime and keeps each class small.

A real-life example

A team calls LLMs from several providers. Retries, timing and logging are the same for all of them; only the HTTP details differ. A base class holds the shared logic, and each subclass fills in one method:

Python
import timeclass LLMClient:    def complete(self, prompt, retries=2):        for attempt in range(retries + 1):            try:                start = time.perf_counter()                text = self._send(prompt)                  # subclass decides how                ms = (time.perf_counter() - start) * 1000                return f"{text} ({type(self).__name__}, {ms:.0f} ms)"            except ConnectionError:                if attempt == retries:                    raise    def _send(self, prompt):        raise NotImplementedErrorclass FakeClient(LLMClient):                               # used in unit tests    def _send(self, prompt):        return f"echo: {prompt}"print(FakeClient().complete("hello"))      # echo: hello (FakeClient, 0 ms)

Adding a new provider means writing one _send method; retries and timing come for free. This "template method" pattern is exactly how PyTorch works: nn.Module handles parameters, devices and hooks, and your subclass only overrides forward.

Follow-up questions to expect

  • "What is the MRO?" — The method resolution order: the list of classes Python searches, left to right, to find an attribute. See it with ClassName.__mro__.
  • "Composition vs inheritance?" — Inheritance models "is-a" and shares an interface; composition models "has-a" and is usually more flexible. Prefer composition unless the "is-a" is real.
  • "What does super() do with multiple inheritance?" — It calls the next class in the MRO, which lets cooperative classes each run once.