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

Inheritance & dunder methods

POST 1 of 5 MorningPythonConcept

Composition over inheritance — read this once

If there's one piece of OO advice that's universally right and universally ignored, it's this: prefer composition over inheritance. Most code that breaks under inheritance breaks because someone reached for inheritance when composition would have been simpler.

The symptom — class hierarchies more than two levels deep. Animal → Mammal → Dog → GoldenRetriever. By level three, you're tying changes at level zero to behaviour at level three, and small modifications cascade in ways you didn't predict.

The fix — favour 'has-a' over 'is-a'.

Old instinct: 'A WeightedSampler is-a Sampler', so make WeightedSampler extend Sampler.

New instinct: 'A WeightedSampler has-a Sampler under the hood', so pass the sampler in via __init__ and call it.

The difference is subtle in code, profound in consequences. Composition keeps your inheritance tree flat. Each class is responsible for its own state and delegates other concerns to collaborators it received in the constructor. Testing becomes easy — pass a fake collaborator. Refactoring becomes easy — swap one collaborator without touching the others.

When IS inheritance the right tool?

Framework hooks. PyTorch's nn.Module, Django's models.Model, Flask's views — these are designed to be subclassed. The framework expects it; the subclass slot is part of the contract.

Tiny mixins for cross-cutting concerns. A SerializableMixin that adds a to_json method. A LoggableMixin that adds debug logging.

Duck typing via Protocols. If you just want polymorphism (different objects responding to the same method), Python's protocols (PEP-544) give you that without requiring inheritance at all.

For your own business logic, default to composition. The day you find yourself debugging multiple inheritance with diamond patterns is the day you wish you had.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#OOP
POST 2 of 5 MiddayPythonDeep dive

The dunder methods that matter

Dunder methods (double-underscore methods, like __init__ and __repr__) are Python's way of letting your classes integrate with built-in syntax and built-in functions.

When you write len(my_obj), Python calls my_obj.__len__(). When you write for x in my_obj, Python calls my_obj.__iter__(). When you write my_obj == other, Python calls my_obj.__eq__(other). Implement the dunder; participate in the syntax.

The ones worth learning first, in priority order:

__init__ — construction. Already covered.

__repr__ — debug string. Already covered.

__eq__ — equality. By default, two objects are equal only if they're the same object in memory. Override __eq__ to compare by value. The moment you override __eq__, you must also override __hash__ — otherwise your objects can't be put in sets or used as dict keys safely. Either return hash of a tuple of fields, or set __hash__ = None to mark the class unhashable.

__len__ — len(). Implement on any class that has a sensible 'size'.

__iter__ — for-loop and iteration. Implement to make for x in my_obj work. Returns an iterator.

__getitem__ — square-bracket access. Implement to make my_obj[i] work. Combine with __len__ and you have a sequence.

__enter__ / __exit__ — with-statement support. Cover Day 12 in detail.

__call__ — make instances callable. obj() then invokes obj.__call__(). Powerful for stateful function-like objects (PyTorch modules use this).

Implement these and your class behaves like a native Python type. That's the goal. Not 'class-shaped'. Native.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonAdvanced
POST 3 of 5 AfternoonPythonCode

A class that behaves like a native type

Look at this TokenStream class. By implementing three dunder methods — __len__, __iter__, __getitem__ — it becomes a sequence in every way Python cares about.

len(ts) works. ts[0] works. for tok in ts: works. list(ts) materialises it. ts[1:3] gives you a slice (because __getitem__ accepts slice objects, not just ints). All without any extra ceremony.

This is the 'protocol' approach in Python. Don't inherit from a Sequence base class. Just implement the methods the protocol requires. Python's duck typing does the rest.

Why this is powerful in real codebases:

NumPy arrays do this. Implementing __len__, __getitem__, and a few more makes their API feel native.

Pandas DataFrames do this. df[col] uses __getitem__. len(df) uses __len__. for col in df: uses __iter__ and yields column names.

PyTorch tensors do this. The reason 't[0]' and 'for x in t' work on tensors is because the Tensor class implements these dunders.

Your code can do this too. Got a custom collection? A custom dataset? A wrapper around an external API that returns a list of things? Implement the protocols and your callers get a native experience.

A tip: collections.abc has abstract base classes (Sequence, Iterable, MutableSet, etc) that document each protocol. You don't have to inherit from them — Python doesn't require it — but reading them tells you exactly which dunders to implement for which behaviour.

Make your classes feel like Python. Not like Java that compiled to Python.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonInternals
POST 4 of 5 EveningPythonTip

If you override __eq__, override __hash__

There's a subtle Python rule that's easy to miss and causes baffling bugs when you do — if you override __eq__, you MUST also override __hash__ (or explicitly set it to None).

Why? Because Python's set and dict use both. When you put an object in a set, Python calls __hash__ to find the right hash bucket. When two objects collide in the same bucket, Python calls __eq__ to disambiguate.

The contract is: if a == b, then hash(a) == hash(b). Two objects that compare equal MUST hash equal. Otherwise dicts and sets behave erratically — same object can be 'in' the set sometimes and not other times, depending on bucket placement.

By default, Python's __hash__ uses object identity (id()), and __eq__ uses object identity too. Override one, leave the other, and the contract breaks.

The fix is one line. If you've overridden __eq__ to compare by some fields, hash a tuple of the same fields:

def __hash__(self):
    return hash((self.x, self.y))

Now equal objects share a hash. The contract holds. Sets and dicts work correctly.

If your class is mutable and you don't want it usable as a dict key (which is sane — mutable hashable types lead to subtle bugs), set __hash__ = None at the class level. Now any attempt to put an instance in a set raises TypeError, with a clear message.

Dataclasses (Day 13) handle this for you when you use frozen=True. The class becomes immutable, __hash__ is auto-generated from the fields, the contract holds.

The bug only shows up when you put your objects in a set or dict, which often happens long after the class was written. Front-load the discipline. It costs one line.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonGotchas
POST 5 of 5 NightPythonRecap

Day 9 — your classes can speak Python's protocols

End of Day 9. Inheritance and dunders are the difference between writing 'class-shaped Python' and writing Python that actually feels like Python.

What we covered.

Morning, composition over inheritance. The advice is universal, the practice is uncommon. Reach for inheritance when you genuinely have an is-a relationship, framework hooks demand it, or you're writing a small mixin. For everything else, pass collaborators in __init__ and delegate. Tests are easier, refactoring is easier, the cognitive load is lower.

Midday, the eight dunder methods that matter. Each one is a hook into Python syntax — len(), for, ==, with, (), and so on. Implement them and your class participates as a first-class citizen.

Afternoon, a TokenStream class that became a real Python sequence by implementing three dunders. NumPy, pandas, PyTorch all use this exact pattern at scale. You can too.

Evening, the __eq__ + __hash__ contract. The bug-class that bites teams when classes meet sets and dicts after months of working fine. Front-load the discipline — override both, or set __hash__ = None.

A pattern across the day. Python's OO design centers on protocols, not class hierarchies. The thing your class needs to 'be' is defined by which methods it implements, not which base class it inherits. This is duck typing, formalised.

Tomorrow, Day 10, we make decorators boring. Decorators are the part of Python that scares beginners and confuses intermediates, but they're really just functions that take functions and return functions. Once you internalise that, every Flask route, every PyTorch optimiser, every pytest fixture stops being magic.

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