Object-Oriented Python Done Right
You already know how to write functions and loops. The next jump in power is organizing related data and behavior together so your programs stay readable as they grow. That is what classes are for. In data and AI work, classes show up everywhere: a model wrapper, a dataset loader, a config object, an API client. This lesson teaches you to write classes that are clean, not clever.
This course is the layer above Python for AI & Data Science. We assume you already know variables, loops, functions, and the basic types. We will not re-teach those.
What You'll Learn
- When a class is the right tool (and when a function or dict is better)
- How
__init__, attributes, and methods fit together - The difference between instance state and class-level data
- How to keep classes small and focused
A Class Is Data Plus the Behavior That Acts on It
Imagine you are tracking experiments while tuning a model. You could juggle parallel lists, but that gets fragile fast. A class bundles the values and the operations into one named thing.
class Experiment:
def __init__(self, name, accuracy):
self.name = name
self.accuracy = accuracy
def passed(self, threshold=0.8):
return self.accuracy >= threshold
run = Experiment("baseline", 0.83)
print(run.name) # baseline
print(run.passed()) # True
__init__ runs when you create the object. self is the specific object being built, and self.name = name stores a value on that object. Methods like passed are just functions that receive self, so they can read and act on the object's own data.
Instance State vs Class-Level Data
Anything you set with self. belongs to one object. Anything defined directly in the class body is shared by every instance. This trips up beginners constantly.
class Model:
provider = "generic" # shared by all instances
def __init__(self, name):
self.name = name # unique per instance
a = Model("classifier")
b = Model("regressor")
print(a.name, b.name) # classifier regressor
print(a.provider, b.provider) # generic generic
A common trap: never use a mutable default like a list at the class level expecting each object to get its own. They will all share the same list. Initialize mutable state inside __init__ instead.
When NOT to Use a Class
Classes are not free. If you only need to pass a few values around, a plain dictionary or a function is often clearer. Reach for a class when:
- The data and the behavior naturally belong together
- You will create many instances of the same shape
- You want to hide internal details behind clean method names
If a class has one method and no real state, it probably wants to be a function.
Pick the simplest tool that fits the job.
| Criteria | Plain function | Class |
|---|---|---|
| Best for | A single transformation on inputs | Data plus behavior, reused as many instances |
| Holds state | No (stateless) | Yes, on self |
| Signal it's the wrong choice | You keep passing the same 4 args everywhere | One method, no real state |
Plain function
- Best for
- A single transformation on inputs
- Holds state
- No (stateless)
- Signal it's the wrong choice
- You keep passing the same 4 args everywhere
Class
- Best for
- Data plus behavior, reused as many instances
- Holds state
- Yes, on self
- Signal it's the wrong choice
- One method, no real state
Inheritance: Use It Sparingly
Inheritance lets a class reuse another's behavior. It is useful, but beginners overuse it and build deep, tangled hierarchies. A good rule: prefer composition (one object holding another) over inheritance unless there is a true "is-a" relationship.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "..."
class Dog(Animal):
def speak(self): # override the parent method
return "Woof"
print(Dog("Rex").speak()) # Woof
Dog is genuinely an Animal, so inheritance fits. If you find yourself inheriting just to reuse one helper method, pass that helper in as an object instead.
Try It
Run this, then add a summary method that returns a readable one-line string for the experiment.
Key Takeaways
- A class bundles related data (
self.attributes) with the behavior that acts on it. __init__builds the object;selfrefers to that specific instance.- Class-body values are shared across all instances; never put mutable defaults there.
- Use a function or dict for simple cases; reach for a class when data and behavior belong together.
- Prefer composition over deep inheritance, and only inherit for true "is-a" relationships.

