A class can be built on top of another class, inheriting its methods and attributes and adding or overriding what it needs:
class Shape:
def area(self):
raise NotImplementedError
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
class Square(Shape): means Square inherits from Shape — every Square is a Shape, and automatically has any methods Shape defines, unless Square provides its own version (which it does here for area, overriding the parent's).
Shape.area raising NotImplementedError is a common pattern: it marks Shape as a base class that's not meant to be used directly — only its subclasses (Square, or a Circle you might add) are meant to actually be instantiated and used, each providing its own concrete area().
The payoff is code that works with any shape, without needing to know which specific one:
def total_area(shapes):
return sum(shape.area() for shape in shapes)
total_area([Square(4), Square(2)]) # calls each one's own area() correctly
This works even if Square and a hypothetical Circle compute area() completely differently — total_area doesn't need to know or care, because it's calling .area() on whatever object it's given, and each object knows how to compute its own.
"Dunder" is short for "double underscore" — methods like __init__, named with leading and trailing double underscores, that Python calls automatically in specific situations, rather than you calling them by name.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __str__(self):
return f"({self.x}, {self.y})"
__eq__(self, other) is called automatically when you write point1 == point2 — without defining it, == would just check whether two objects are the exact same object in memory, not whether their contents match.__str__(self) is called automatically by str(point) and print(point), controlling how the object displays as text.__len__(self) is called by len(obj).__contains__(self, item) is called by item in obj.The pattern across all of them: define the dunder method once on your class, and Python's built-in syntax (==, str(), len(), in) automatically routes to it for objects of that class — you get to use familiar operators on your own custom objects, instead of inventing your own method names like .equals() or .length().