S
Saurav Danej
90-Day AI/ML LinkedIn Content System
← All days
8
Day 8 of 90Python

OOP — when (and when not) to use classes

POST 1 of 5 MorningPythonConcept

What a Python class actually is

Week 2 starts with a topic most people make harder than it needs to be — Python classes.

A class is a factory that produces objects with shared behaviour. That's the whole concept. Stripped of the formalism, you're just saying 'here's a recipe for making things; each thing has its own data and shares the same methods'.

The one piece that confuses everyone is self. Self isn't magic, isn't a keyword, isn't enforced by the runtime. It's just the name we conventionally give to the first parameter of every method, and Python passes the instance into that slot when you call obj.method(). 'def greet(self):' is just shorthand for 'def greet(instance_of_this_class):'.

You could call it 'this', or 'me', or 'me_obj'. The convention is self, and following it is non-negotiable in any team codebase. But knowing why it's there demystifies the whole class system.

The rest of OOP in Python rests on three things.

__init__ — the constructor. Runs once when you create an object. Use it to set the instance's initial attributes (self.x = x, self.y = y).

Attributes — data stored on the instance. Accessed via self.x inside methods, obj.x outside. Type hints make them readable.

Methods — functions defined in the class body that take self as the first argument. Bound to the class so every instance can call them.

That's 90% of OOP in Python. The remaining 10% is conventions — how to name private attributes (single underscore prefix), when to use class methods versus static methods (rarely), how to override dunder methods to integrate with built-ins (tomorrow's topic).

Don't overengineer. Most useful classes are 30 lines and three methods.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#OOP
POST 2 of 5 MiddayPythonDeep dive

When to use a class — and when not

Java taught millions of developers that everything must be a class. Python is gentler — it gives you classes when you want them, modules and functions when you don't.

Unlearning the 'class everything' reflex is a one-week project. Worth it.

Reach for a class when:

You have state and behaviour that travel together. A user session that holds tokens AND knows how to refresh them. An ML model that holds weights AND knows how to predict. State+behaviour pair = class.

You need multiple instances. If there's exactly one of something in the program, a module-level dict is often cleaner than a singleton class.

You want a clear lifecycle — open, use, close. A database connection, a file handle, a model session. The class formalises 'this thing has a beginning and end'.

You'll have subclasses. If callers need to swap implementations (real DB vs fake DB for testing), a base class with concrete subclasses is the right shape.

DON'T reach for a class when:

You have a group of utility functions. Use a module. Functions in a module are namespaced (utils.parse_date() reads fine). A class with all-static methods is a Java reflex.

You'd write one method called 'run' or 'execute'. That's a function pretending to be a class. Just write the function.

You only have data, no behaviour. Use a dataclass (Day 13) or a NamedTuple. Same shape, way less code, immutable by default if you want it.

You're using inheritance to share helper methods. Composition (passing helpers in __init__) is almost always cleaner.

A module + a dataclass + a few functions beats a class hierarchy 8 times out of 10. Save classes for when they earn their weight.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#CleanCode
POST 3 of 5 AfternoonPythonCode

A minimal class, the right way

Here's a 12-line class that demonstrates 80% of what most production Python classes need.

Look at the structure. __init__ takes self plus the constructor args. Attributes get set on self with explicit types. The cosine method takes self plus another Embedding and returns a float. There's a __repr__ at the bottom for debug output.

A few things to notice.

Type hints on every parameter and return type. The 'list[float]' is Python 3.9+ syntax for a list of floats. The 'Embedding' in the cosine signature is the class itself — Python lets you reference your own class in a method's signature using a forward reference (the quotes are needed in older Python; 3.12+ lets you skip them in many cases).

The cosine method does the math inline. For a real codebase you'd use NumPy (a @ b / (np.linalg.norm(a) * np.linalg.norm(b))) which we'll cover Day 30. The pure-Python version makes the formula visible.

The __repr__ gives a useful debug string. Without it, print(emb) shows '<Embedding object at 0x10ab9f8e0>'. With it, print(emb) shows Embedding(text='hello', dim=384). The difference is hours of debugging time over a project's lifetime.

This is what 'a Python class' looks like in well-written code. No unnecessary inheritance. No abstract base class layer. No 'Manager' suffix. Just data, behaviour, and a useful repr.

If your classes are starting to look more elaborate than this and you can't articulate why, simplify. Most classes that justify their existence are small.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonOOP
POST 4 of 5 EveningPythonTip

Always write __repr__ first

Tiny habit, massive payoff — write __repr__ before any business logic on a new class.

Why? Because the moment you write a real method, you'll want to debug it. And debugging without __repr__ looks like this:

print(my_obj)  # <MyClass object at 0x10ab9f8e0>

Wonderful. So informative. Now you have to print individual fields to see what's actually inside.

With __repr__:

print(my_obj)  # MyClass(name='Saurav', count=3, status='active')

Now your debugger output is meaningful. Logging is meaningful. Error messages are meaningful. Pytest's failure output is meaningful.

The minimum useful __repr__ is one line. Class name plus the two or three most important fields.

def __repr__(self) -> str:
    return f'{type(self).__name__}(name={self.name!r}, count={self.count})'

Note the !r in the f-string. That's repr formatting — wraps strings in quotes so 'foo' shows as "'foo'" not 'foo'. Critical for distinguishing 'None' (the string) from None (the value). Use !r for any field that might be a string, list, or dict.

A class without __repr__ is a class you'll regret in three months. Write the one-liner upfront. Cost: 30 seconds. Lifetime savings: hours.

Bonus tip — dataclasses (Day 13) generate __repr__ automatically. If you find yourself writing __repr__ a lot, you probably want a dataclass.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonTips
POST 5 of 5 NightPythonRecap

Day 8 — class with intent, not by reflex

End of Day 8. Week 2 begins. We're now in 'how Python is actually written in production' territory.

What we covered.

Morning, the foundations. A class is a factory that produces objects with shared behaviour. Self is just the first parameter every method receives — Python passes the instance into it. Once you stop seeing self as magic, classes get simpler.

Midday, the most useful guidance about classes — when to use them and when not. Over-classing is a Java reflex. Reach for a class when you have state plus behaviour together, multiple instances, a clear lifecycle, or genuine subclassing needs. Otherwise reach for a function, module, or dataclass.

Afternoon, a 12-line class that demonstrates almost everything you need. Type hints, __init__, an actual method, and a __repr__. Most production classes look about this clean. If yours look more elaborate without justification, simplify.

Evening, the cheapest debugging upgrade in Python — write __repr__ first. One-liner cost. Hours of saved debugging across a project's life.

A broader observation. Python's OOP is not Java's OOP. Python wants you to use the right tool — function, dataclass, class, module — for each situation. Classes aren't the default; they're one option. The reflex to wrap everything in a class is a habit from other languages. Unlearn it; the result is cleaner code.

Tomorrow, Day 9, we go deeper — inheritance and dunder methods. The methods that make your objects 'feel native' to Python (work with len(), iter(), with-statements). The single most powerful idea in Python OOP, and the one most beginners skip.

See you in the morning.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonOOP