Programming with Python | Chapter 14: OOP – More on Methods and Dunder Methods
Chapter Objectives
- Distinguish between instance methods, class methods, and static methods.
- Learn how to define and use class methods using the
@classmethoddecorator. - Understand the role of the
clsparameter in class methods. - Learn how to define and use static methods using the
@staticmethoddecorator. - Understand dunder methods (special methods with double underscores, e.g.,
__init__). - Implement common dunder methods to customize object behavior:
__str__: For user-friendly string representation (used byprint()).__repr__: For unambiguous, developer-focused string representation.__len__: To allowlen()to work on objects.__eq__: To define equality comparison (==) between objects.
Introduction
In previous chapters, we focused on instance methods – functions bound to specific objects, operating on their unique state via the self parameter. This chapter expands our understanding of methods by introducing class methods, which operate on the class itself, and static methods, which are related to the class but don’t depend on the instance or class state. We will also explore dunder methods (short for “double underscore” methods), like the __init__ method we’ve already seen. These special methods allow us to integrate our custom objects more seamlessly with Python’s built-in functions and operators, defining how they should be represented as strings, compared for equality, or measured for length.
Theory & Explanation
Method Types Recap
Instance Methods:
- Most common type.
- Operate on an instance (object) of the class.
- First parameter is always
self, representing the instance. - Can access and modify both instance attributes
(self.attribute)and class attributes (self.class_attributeorClassName.class_attribute). - Defined without any special decorator.
class MyClass:
def instance_method(self, arg1):
print(f"Instance method called on {self} with {arg1}")
# Access self.some_attribute, etc.Class Methods:
- Operate on the class itself, rather than on a specific instance.
- Defined using the
@classmethoddecorator above the method definition. - First parameter is always
cls(by convention), representing the class itself. - Cannot directly access instance attributes (as they belong to specific objects), but can access and modify class attributes (
cls.class_attribute). - Often used as alternative constructors or for operations related to the class as a whole.
class MyClass:
class_var = 10
@classmethod
def class_method(cls, arg1):
print(f"Class method called on {cls} with {arg1}")
print(f"Accessing class var: {cls.class_var}")
# Cannot access self.instance_attribute heregraph TD
A["Call: object.method()"] --> B{Is method decorated?};
B -- "@classmethod" --> C["Pass Class (cls) to method"];
B -- "@staticmethod" --> D[Pass only explicit args];
B -- "No Decorator" --> E["Pass Instance (self) to method"];
F["Call: Class.method()"] --> G{Is method decorated?};
G -- @classmethod --> C;
G -- @staticmethod --> D;
G -- No Decorator --> H[TypeError: missing 'self'];
C --> Z[Execute Method];
D --> Z;
E --> Z;
subgraph Instance Method
E
end
subgraph Class Method
C
end
subgraph Static Method
D
end
Static Methods:
- Don’t operate on the instance (
self) or the class (cls). They are essentially regular functions namespaced within the class. - Defined using the
@staticmethoddecorator. - Do not receive
selforclsautomatically as the first argument. - Cannot directly access instance or class attributes (unless explicitly passed the instance/class or accessing globals).
- Used for utility functions that are logically related to the class but don’t depend on its state.
class MyClass:
@staticmethod
def static_method(arg1, arg2):
print(f"Static method called with {arg1}, {arg2}")
# Cannot access self or cls here
# Often performs some calculation based only on arguments
return arg1 + arg2Calling Methods:
- Instance methods:
my_object.instance_method(arg) - Class methods:
MyClass.class_method(arg)ormy_object.class_method(arg)(calling on an instance still passes the class ascls) - Static methods:
MyClass.static_method(arg1, arg2)ormy_object.static_method(arg1, arg2)
Dunder Methods (Special Methods)
Python classes can implement special methods with double underscores at the beginning and end of their names (e.g., __init__, __str__). These are often called “dunder” methods. They are not typically called directly by your code (e.g., you don’t usually write my_object.__str__()). Instead, Python calls them implicitly in response to certain operations or built-in functions. Implementing these methods allows your custom objects to behave more like built-in types.
1. __str__(self):
- Purpose: Returns an informal, user-friendly string representation of the object.
- Called By:
print(my_object),str(my_object). - Return Value: Must return a string (
str). - If not defined: Python falls back to using
__repr__.
2. __repr__(self):
- Purpose: Returns an unambiguous, official string representation of the object, often useful for debugging. Ideally,
eval(repr(my_object))should recreate the object (though not always feasible). - Called By: Directly typing the object name in the REPL,
repr(my_object), and as a fallback for__str__. - Return Value: Must return a string (
str). - If not defined: Python provides a default representation like
<__main__.ClassName object at 0x...>.
Convention: Always try to implement __repr__. If __str__ is not implemented, __repr__ will be used for print() as well. If __str__ is implemented, print() will use it, while the REPL and repr() will still use __repr__.
3. __len__(self):
- Purpose: Returns the “length” of the object, often interpreted as the number of items it contains.
- Called By:
len(my_object). - Return Value: Must return a non-negative integer (
int). - If not defined: Calling
len()on the object raises aTypeError.
4. __eq__(self, other):
- Purpose: Defines behavior for the equality operator (
==). Comparesselfwithother. - Called By:
self == other. - Return Value: Should return
Trueif objects are considered equal,Falseotherwise. Can also returnNotImplementedif comparison isn’t supported for the givenothertype (allowing Python to tryother.__eq__(self)). - If not defined: Default behavior compares object identities (memory addresses), meaning two distinct objects are never equal even if their attributes are identical.
There are many other dunder methods for arithmetic operators (__add__, __sub__), comparison (__lt__, __gt__), attribute access (__getattr__, __setattr__), container emulation (__getitem__, __setitem__), etc., allowing extensive customization.
graph TD
%% Nodes (declared first, no links yet)
print_obj["print(obj)"]
repr_obj["repr(obj) or REPL output"]
has_str{{Has obj.__str__?}}
use_str["Use obj.__str__()"]
has_repr{{Has obj.__repr__?}}
use_repr["Use obj.__repr__()"]
use_default["Use default object representation"]
output_str["User-friendly string"]
output_repr["Developer-focused string"]
output_default["ClassName object at hex address"]
%% Subgraphs (only for grouping, no arrows inside)
subgraph User_Action
print_obj
repr_obj
end
subgraph Python_Internals
has_str
use_str
has_repr
use_repr
use_default
end
subgraph Output
output_str
output_repr
output_default
end
%% Links (must be outside subgraphs in v11.5.0)
print_obj --> has_str
has_str -- Yes --> use_str
has_str -- No --> has_repr
has_repr -- Yes --> use_repr
has_repr -- No --> use_default
repr_obj --> has_repr
use_str --> output_str
use_repr --> output_repr
use_default --> output_default
Code Examples
Example 1: Class Methods and Static Methods
# employee_methods.py
import datetime
class Employee:
"""Represents an employee with different method types."""
# Class attribute
num_employees = 0
raise_amount = 1.04 # 4% raise
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = float(pay)
self.email = f"{first.lower()}.{last.lower()}@company.com"
Employee.num_employees += 1 # Increment class attribute
# Instance method
def fullname(self):
return f"{self.first} {self.last}"
# Instance method
def apply_raise(self):
self.pay = self.pay * self.raise_amount # Access instance specific raise? No, see class method
# Class method - often used as alternative constructors
@classmethod
def set_raise_amount(cls, amount):
"""Sets the raise amount for ALL employees."""
print(f"Setting raise amount to {amount} for class {cls.__name__}")
cls.raise_amount = amount
@classmethod
def from_string(cls, emp_str):
"""Parses employee data from 'first-last-pay' string."""
first, last, pay = emp_str.split('-')
# Creates and returns a new Employee instance using the class (cls)
return cls(first, last, pay)
# Static method - logically related but doesn't depend on instance/class state
@staticmethod
def is_workday(day):
"""Checks if a given date is a workday (Mon-Fri)."""
# datetime.weekday(): Monday is 0 and Sunday is 6
if day.weekday() == 5 or day.weekday() == 6: # Saturday or Sunday
return False
return True
# --- Using the methods ---
print(f"Initial number of employees: {Employee.num_employees}") # 0
emp1 = Employee("Corey", "Schafer", 50000)
emp2 = Employee("Test", "User", 60000)
print(f"Number of employees: {Employee.num_employees}") # 2
# Using instance methods
print(emp1.fullname())
print(emp2.email)
# Using a class method (called on the class)
Employee.set_raise_amount(1.05) # Set raise to 5% for all
# Accessing the class attribute via instance or class shows the change
print(f"Emp1 raise amount: {emp1.raise_amount}") # 1.05
print(f"Emp2 raise amount: {emp2.raise_amount}") # 1.05
print(f"Employee raise amount: {Employee.raise_amount}") # 1.05
# Using class method as alternative constructor
emp_str_3 = "John-Doe-70000"
emp3 = Employee.from_string(emp_str_3)
print(f"\nCreated employee from string: {emp3.fullname()}, Pay: {emp3.pay}")
print(f"Number of employees: {Employee.num_employees}") # 3
# Using static method
my_date = datetime.date(2025, 4, 12) # A Saturday
print(f"\nIs {my_date} a workday? {Employee.is_workday(my_date)}") # False
my_date = datetime.date(2025, 4, 14) # A Monday
print(f"Is {my_date} a workday? {Employee.is_workday(my_date)}") # TrueExplanation:
apply_raise(instance method) usesself.raise_amount. If we wanted it to always use the current class raise amount, it should useEmployee.raise_amountorself.__class__.raise_amount.set_raise_amount(@classmethod) takesclsand modifies the class attributecls.raise_amount, affecting all future calculations that use it.from_string(@classmethod) acts as an alternative way to createEmployeeinstances, parsing a string and callingcls(first, last, pay)which is equivalent toEmployee(first, last, pay).is_workday(@staticmethod) performs a calculation based only on its input (day) and doesn’t needselforcls. It’s logically grouped withEmployeebut doesn’t depend on employee data or the employee count.
Example 2: Implementing Dunder Methods
# vector2d.py
import math
class Vector2D:
"""Represents a 2D vector with x and y components."""
def __init__(self, x=0, y=0):
if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):
raise TypeError("Vector components must be numeric")
self.x = x
self.y = y
def magnitude(self):
"""Calculates the magnitude (length) of the vector."""
return math.sqrt(self.x**2 + self.y**2)
# --- Dunder Methods ---
def __str__(self):
"""User-friendly string representation."""
# Used by print()
return f"Vector({self.x}, {self.y})"
def __repr__(self):
"""Unambiguous developer representation."""
# Used by REPL, repr()
# Aims to show how to recreate the object
return f"Vector2D(x={self.x}, y={self.y})"
def __len__(self):
"""Returns the 'length' - we'll define it as integer magnitude."""
# Used by len()
# Must return an integer
return int(self.magnitude())
def __eq__(self, other):
"""Checks for equality (self == other)."""
# Used by == operator
if isinstance(other, Vector2D):
# Compare components for equality
return self.x == other.x and self.y == other.y
return NotImplemented # Indicate comparison not supported for other types
def __add__(self, other):
"""Defines vector addition (self + other)."""
# Used by + operator
if isinstance(other, Vector2D):
# Return a *new* Vector2D object
return Vector2D(self.x + other.x, self.y + other.y)
raise TypeError("Can only add Vector2D to another Vector2D")
# --- Using the Vector2D class ---
v1 = Vector2D(3, 4)
v2 = Vector2D(3, 4)
v3 = Vector2D(1, 2)
v4 = Vector2D() # Uses default x=0, y=0
# __str__ usage
print(v1) # Output: Vector(3, 4)
print(str(v3)) # Output: Vector(1, 2)
# __repr__ usage (e.g., in REPL or using repr())
print(repr(v1)) # Output: Vector2D(x=3, y=4)
print([v1, v3]) # Output: [Vector2D(x=3, y=4), Vector2D(x=1, y=2)] (uses repr in containers)
# __len__ usage
print(f"Magnitude of v1: {v1.magnitude():.2f}") # 5.00
print(f"Length (int magnitude) of v1: {len(v1)}") # Output: 5
# __eq__ usage
print(f"v1 == v2: {v1 == v2}") # Output: True (attribute values match)
print(f"v1 == v3: {v1 == v3}") # Output: False
print(f"v1 == (3, 4): {v1 == (3, 4)}") # Output: False (types don't match, __eq__ returns NotImplemented)
# __add__ usage
v_sum = v1 + v3
print(f"v1 + v3 = {v_sum}") # Output: v1 + v3 = Vector(4, 6) (uses __str__)
print(repr(v_sum)) # Output: Vector2D(x=4, y=6) (uses __repr__)
# try:
# v_bad_sum = v1 + 5 # Raises TypeError defined in __add__
# except TypeError as e:
# print(e)
Explanation:
- The
Vector2Dclass defines__init__. __str__provides a simpleVector(x, y)format for printing.__repr__provides a more detailedVector2D(x=..., y=...)format, useful for debugging and showing how the object was likely created.__len__allowslen(vector_object)to work, returning the integer part of the vector’s magnitude.__eq__defines that twoVector2Dobjects are equal if their correspondingxandycomponents are equal. It returnsNotImplementedif compared with a non-Vector2Dtype.__add__defines vector addition using the+operator, returning a newVector2Dobject representing the sum.
Common Mistakes or Pitfalls
@classmethod/@staticmethodDecorators: Forgetting to add the@classmethodor@staticmethoddecorator when defining class or static methods, leading to errors related to the implicitselfparameter being passed (or expected but not received).Confusing cls and self:Usingselfin a class method orclsin an instance method incorrectly.- Incorrect Dunder Method Signatures: Defining dunder methods with the wrong parameters (e.g.,
__eq__(self)instead of__eq__(self, other)), causing them not to work as expected with built-in operations. __str__vs__repr__: Not implementing__repr__, leading to unhelpful default object representations. Or implementing only__str__and getting confusing output when debugging or viewing objects in containers (which often use__repr__).__eq__without Type Checking: Implementing__eq__without checking the type ofother(isinstance(other, ClassName)) can lead toAttributeErrorif you try to access attributes onotherthat don’t exist (e.g., comparing your object to an integer). ReturningNotImplementedis crucial for interoperability.- Modifying Objects in Dunder Methods (Unexpectedly): Methods like
__add__should generally return a new object representing the result, not modifyselforotherin place (unless specifically implementing in-place operators like__iadd__for+=).
Chapter Summary
| Concept | Decorator / Syntax / Called By | Key Characteristics |
|---|---|---|
| Instance Method | def method(self, ...): |
Operates on an instance (self). Can access/modify instance and class attributes. Most common method type. Called via object.method(). |
| Class Method | @classmethoddef method(cls, ...): |
Operates on the class (cls). Can access/modify class attributes but not instance attributes directly. Often used for alternative constructors or class-specific operations. Called via Class.method() or object.method(). |
| Static Method | @staticmethoddef method(...): |
Doesn’t operate on instance (self) or class (cls). Like a regular function namespaced within the class. Used for utility functions logically related to the class. Called via Class.method() or object.method(). |
__init__(self, ...) |
Called automatically when creating an instance: Class(...) |
The constructor; initializes the new object’s state (instance attributes). |
__str__(self) |
Called by print(object), str(object) |
Returns an informal, user-friendly string representation. Should return a str. Falls back to __repr__ if not defined. |
__repr__(self) |
Called by REPL output, repr(object), fallback for __str__, used in containers |
Returns an unambiguous, developer-focused string representation, ideally showing how to recreate the object. Should return a str. Always recommended to implement. |
__len__(self) |
Called by len(object) |
Returns the “length” of the object (e.g., number of items). Must return a non-negative int. Raises TypeError if not defined. |
__eq__(self, other) |
Called by the equality operator: self == other |
Defines custom equality comparison. Should return True, False, or NotImplemented. Default compares object identity (memory address). |
__add__(self, other) |
Called by the addition operator: self + other |
Defines behavior for the + operator. Typically returns a new object representing the sum. Should handle incompatible types (e.g., raise TypeError or return NotImplemented). |
- Classes can have instance methods (operate on
self), class methods (@classmethod, operate oncls), and static methods (@staticmethod, operate independently). - Class methods are often used as alternative constructors or for operations on class-level data.
- Static methods are utility functions grouped within a class namespace.
- Dunder methods (
__name__) allow objects to integrate with Python’s built-in functions and operators. __str__(self)provides a user-friendly string representation (forprint()).__repr__(self)provides a developer-focused, unambiguous string representation. Implement this first.__len__(self)enables thelen()function for your objects.__eq__(self, other)defines the behavior of the equality operator (==).- Implementing dunder methods makes custom classes more intuitive and Pythonic.
Exercises & Mini Projects
Exercises
CounterClass: Create a classCounterwith a class attributecountinitialized to 0. Implement__init__(self)that incrementsCounter.count. Add a class methodget_count()that returns the value ofCounter.count. Create a few instances and print the count using the class method.TemperatureConverter: Create a classTemperaturewith an__init__(self, celsius)that stores temperature in Celsius. Add a static methodcelsius_to_fahrenheit(celsius)that converts Celsius to Fahrenheit (F = C * 9/5 + 32). Add an instance methodget_fahrenheit(self)that uses the static method to return the Fahrenheit equivalent of the instance’s Celsius temperature.__str__and__repr__: Add__str__and__repr__methods to thePersonclass created in Chapter 12 Exercise 1.__str__should return something like “Person: [Name], Age: [Age]”.__repr__should return something likePerson(name='[Name]', age=[Age]). Create an instance and test bothprint(person)andrepr(person).__len__forWordList: Create a classWordListthat takes a sentence string in its__init__. Store the words (split the sentence) in an instance attribute list calledself.words. Implement the__len__(self)method to return the number of words in the list. Create an instance and testlen(word_list_object).__eq__forPoint: Create a simplePointclass with__init__(self, x, y)storing x and y coordinates. Implement the__eq__(self, other)method to returnTrueif both the x and y coordinates ofselfandother(anotherPointobject) are equal,Falseotherwise. Create severalPointobjects and test equality (==).
Mini Project: Enhancing the LibraryItem Hierarchy
Goal: Add useful dunder methods and potentially a class method to the LibraryItem hierarchy from Chapter 13’s mini-project.
Steps:
- Retrieve Classes: Start with the
LibraryItem,Book, andDVDclasses from Chapter 13. Add __str__ and __repr__:- Implement
__str__(self)in theLibraryItembase class. It should return a user-friendly string like “Item: [Title] ([Identifier])”. - Implement
__repr__(self)inLibraryItem. It should return something unambiguous, likeLibraryItem(title='[Title]', identifier='[Identifier]'). - Override
__repr__in both theBookandDVDsubclasses to provide representations specific to them (including author/pages or director/duration), e.g.,Book(title='...', identifier='...', author='...', num_pages=...). You might not need to override__str__if the base class version is sufficient, but you can if desired.
- Implement
Add __eq__:- Implement
__eq__(self, other)in theLibraryItembase class. Two library items should be considered equal if they are of the same type (e.g., bothBookor bothDVD) AND theiridentifierattributes are the same. Useisinstance()to check types and compareself.identifier == other.identifier. ReturnNotImplementedifotheris not aLibraryItem.
- Implement
- (Optional) Class Attribute/Method:
- Add a class attribute
total_items = 0toLibraryItem. - Modify the
LibraryItem.__init__to incrementLibraryItem.total_itemseach time any library item (Book, DVD, etc.) is created. - Add a class method
get_total_items(cls)toLibraryItemthat returns the value ofcls.total_items.
- Add a class attribute
- Instantiate and Test:
- Create several
BookandDVDobjects, including some with the same identifier but different titles/authors (to test__eq__). - Print the objects (tests
__str__). - Print a list containing some objects (tests
__repr__). - Compare objects using
==(tests__eq__). - If you implemented the optional step, call
LibraryItem.get_total_items()to see the total count.
- Create several
Additional Sources:


