class Animal:
# Class attribute (shared by all Animal instances)
kingdom = "Animalia"
def __init__(self, name, age):
self.name = name # instance attribute
self.age = age # instance attribute
# Instance method — works with a specific object instance
def speak(self):
return "hiss"
# Class method — works with the class, not a specific object
@classmethod
def describe_kingdom(cls):
return f"All animals belong to the kingdom: {cls.kingdom}"
# Static method — does NOT use class or instance data
@staticmethod
def is_alive():
return True
Instance attributes: self.attribute, initialized within __init__() (by convention) or any instance methods
Instance methods: Must include self (as the first argument); but not specified when calling the methods, Python passes it automatically
Class attributes: Shared by all instances, class method (@classmethod) can access it
Class methods: Specified with a decorator (@classmethod), and receive class (cls), not instance
Static methods: Specified with a decorator (@staticmethod); no access to class or instance data
a = Animal("Buddy", 3)
print(a.speak()) # Instance method
print(Animal.describe_kingdom()) # Class method
print(Animal.is_alive()) # Static method
print(a.is_alive()) # Also allowed (but not typical)
print(a.kingdom) # Access class attribute through instance
print(Animal.kingdom) # Access class attribute through class
# Inherit from Animal
class Dog(Animal):
def speak(self): # Overriding
base = super().speak() # Calling parent method
return f"{self.name} makes {base} and says woof!"
d = Dog("Buddy")
print(d.speak()) # Buddy make hiss and says woof!
Dunder methods (and attributes) are defined in the class. Their names that start and end with double underscores: __name__
They affect how the object behaves when Python interacts with it. They can act as instance methods, class-level hooks, operator behavior controls, context manager behavior, iterable protocol, method dispatch, etc.
They are part of Python's data model and have special meaning that Python itself uses.
❌ You should not create your own arbitrary __myfunc__ (Unless you're intentionally integrating with Python's object model)
✅ We override existing dunder methods to customize behavior.
❌ You never call dunder methods directly in normal code — Python calls them.
__init__ | Initialize object state | obj = Class(...)
__str__ | Converting to string | print(obj)
__repr__ | representation for debugging | obj in REPL
__len__ | container size | len(obj)
__iter__ | Make object iterable | for x in obj:
__getitem__ | Indexing | obj[0]
__add__ | using + operator | a + b
__eq__ | equality comparison | a == b
__call__ | treating object as function | obj()
This is called when an object is created: obj = Class(...)
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
p = Person("Alice", 30)
print(p.name, p.age) # Alice 30
Defines what print(obj) outputs.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"{self.name}, {self.age} years old"
p = Person("Alice", 30)
print(p) # Alice, 30 years old
Should ideally return a string that could recreate the object.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"Person('{self.name}', {self.age})"
p = Person("Alice", 30)
print(repr(p)) # Person('Alice', 30)
Tip: If you only implement __repr__ and not __str__, print(obj) uses __repr__.
class Team:
def __init__(self, members):
self.members = members
def __len__(self):
return len(self.members)
team = Team(["Alice", "Bob", "Charlie"])
print(len(team)) # 3
class Team:
def __init__(self, members):
self.members = members
def __iter__(self):
return iter(self.members)
team = Team(["Alice", "Bob", "Charlie"])
for member in team:
print(member)
class Adder:
def __init__(self, n):
self.n = n
def __call__(self, x):
return self.n + x
add5 = Adder(5)
print(add5(10)) # 15
class Team:
def __init__(self, members):
self.members = members
def __getitem__(self, index):
# support indexing & slicing
return self.members[index]
def __setitem__(self, index, value):
# support assignment: team[1] = "Bobby"
self.members[index] = value
def __delitem__(self, index):
# optionally support deletion: del team[1]
del self.members[index]
team = Team(["Alice", "Bob", "Charlie"])
print(team[1]) # Bob
team[1] = "Bobby"
print(team[1]) # Bobby
del team[2]
print(team) # Team(['Alice', 'Bobby'])
team[1] = "Bobby"
print(team[1]) # Bobby
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
return self.age == other.age
def __lt__(self, other):
return self.age < other.age
p1 = Person("Alice", 30)
p2 = Person("Bob", 25)
print(p1 == p2) # False
print(p2 < p1) # True
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2
print(p3.x, p3.y) # 4 6
class ManagedFile:
def __init__(self, filename):
self.filename = filename
def __enter__(self):
self.file = open(self.filename, 'w')
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
with ManagedFile('test.txt') as f:
f.write("Hello world!")
__new__ Object creation (before __init__)
__class_getitem__ Enables ClassName[item] (used in typing)
__init_subclass__ Called when a class is subclassed
__mro_entries__ Affects multiple inheritance resolution
Instance-level dunders: How the object acts (len(obj), obj + obj, iteration, printing)
Class-level dunders: How the class itself behaves (subclassing rules, generics, instantiation)
Dunder attributes: Bookkeeping about the object/class
__dict__ Namespace of object
__class__ Its class
__name__ Class or function name
__module__ Where it was defined
__annotations__ Type hints