Special Methods and Polymorphism
Objectives
By the end of this chapter, you should be able to:
- Recognize common special (“dunder”) methods
- Explain polymorphism and give an example
- Use
abcand@abstractmethodto enforce that a subclass implements a method - Extend a built-in type with a custom method
💡 Why this matters: Special methods are how your own classes plug into Python’s built-in syntax (
print(),len(),+, and more) instead of needing their own one-off functions.
Special (“Dunder”) Methods
Methods surrounded by double underscores, called dunder methods, are how Python hooks your class into its built-in syntax and functions. A few of the most common:
| Method | Purpose |
|---|---|
__init__ |
Runs when an instance is created |
__str__ |
Controls what str(obj) and print(obj) display |
__repr__ |
A developer-facing representation, similar in spirit to __str__ |
__len__ |
Controls what len(obj) returns |
__del__ |
Runs when an instance is about to be destroyed |
__doc__ |
The docstring associated with a class |
__class__ |
A reference to the instance’s class |
__format__ |
Controls how an instance is rendered by format() |
You’ll encounter more of these as you read other people’s Python code. Implementing them is what lets a custom class behave like a built-in one.
Polymorphism
Polymorphism means the same operation behaves differently depending on the type it’s applied to. You’ve already used this without naming it:
5 + 3 # 8, numeric addition
"8" + "3" # "83", string concatenation
+ does something different for numbers than it does for strings: that’s polymorphism. len() is another example:
sample_list = [1, 2, 3, 4]
sample_tuple = (1, 2, 3)
sample_string = "hello"
len(sample_list) # 4
len(sample_tuple) # 3
len(sample_string) # 5
One len() function, three different behaviors depending on the type it receives. Polymorphism also shows up between related classes: two classes don’t need a common parent to both implement the same method name:
class Pet:
def talk(self):
raise NotImplementedError("Subclasses must implement talk()")
class Dog(Pet):
def talk(self):
return "WOOF!"
class Cat(Pet):
def talk(self):
return "MEOW!"
Both Dog and Cat implement .talk() differently, but any code that calls .talk() on a Pet doesn’t need to know which subclass it actually has. It just works, which is the whole point of polymorphism.
Formalizing It with Abstract Base Classes
Pet above works, but nothing stops someone from instantiating Pet directly, or forgetting to override .talk() in a new subclass. You’d only find out at runtime, when NotImplementedError actually fires. The abc module makes that contract explicit and enforced, using ABC as a base class and @abstractmethod to mark required methods:
from abc import ABC, abstractmethod
class Pet(ABC):
@abstractmethod
def talk(self):
pass
class Dog(Pet):
def talk(self):
return "WOOF!"
class Cat(Pet):
def talk(self):
return "MEOW!"
Pet() # TypeError: Can't instantiate abstract class Pet with abstract method talk
class Fish(Pet):
pass
Fish() # TypeError: Can't instantiate abstract class Fish with abstract method talk
An ABC subclass with an unimplemented @abstractmethod simply can’t be instantiated at all. Python catches the mistake immediately, at the moment you try to create the object, rather than later when something finally calls .talk() on it. Dog and Cat work exactly as before, since both actually implement talk().
Use plain NotImplementedError for a quick, informal version of this pattern; reach for ABC/@abstractmethod when you want Python itself to enforce that a subclass can’t skip a required method.
Extending a Built-in Type
You can also subclass a built-in type directly, to add your own methods on top of everything it already does:
class ExtendedStr(str):
def first_last_character(self):
return self[0] + self[-1]
s = ExtendedStr("awesome")
s.first_last_character() # "ae"
len(s) # 7, still behaves like a normal string
s.upper() # "AWESOME", inherited straight from str
ExtendedStr is a real subclass of str, so it inherits every normal string method (.upper(), len(), and the rest) and adds first_last_character() on top. Note that a plain string literal like "awesome" is still an ordinary str, not an ExtendedStr, so it does not get the new method:
"awesome".first_last_character() # AttributeError: 'str' object has no attribute 'first_last_character'
You have to actually construct an ExtendedStr to get the extra method. Subclassing a built-in type like this is a real technique, but it’s an approach to reach for sparingly, since most code that expects a plain str neither knows nor cares about your extra method.
Try It
- Add a
__str__method to a class of your own and confirm thatprint()uses it. - Write two unrelated classes that both implement a method with the same name, and call that method on an instance of each without changing your calling code.
- Predict what
len()returns for a dictionary before trying it, then check. - Turn one of your own classes into an
ABCwith an@abstractmethod, then confirm you can’t instantiate it directly until a subclass implements that method.
Recap
- Dunder methods (
__init__,__str__,__len__, and others) are how your class plugs into Python’s built-in syntax and functions. - Polymorphism means the same operation (an operator, a function, a method name) behaves differently depending on the type or class it’s used with.
- Polymorphism doesn’t require inheritance; unrelated classes can each implement a method with the same name.
ABCand@abstractmethodenforce that a subclass implements a required method. Python refuses to instantiate a class that doesn’t, rather than failing later at the call site.- You can subclass a built-in type to add new methods, though it’s an approach to use sparingly.
Next lesson: put it all together with object-oriented programming exercises.