String Representation
repr vs str
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
"""Unambiguous representation for developers."""
return f'Point({self.x}, {self.y})'
def __str__(self):
"""Readable representation for users."""
return f'({self.x}, {self.y})'
p = Point(1, 2)
print(repr(p)) # Point(1, 2) - used in REPL, debuggers
print(str(p)) # (1, 2) - used by print()
print(p) # (1, 2) - uses __str__
# In collections, __repr__ is used
print([p, Point(3, 4)])
# [Point(1, 2), Point(3, 4)]
Best Practice
# ✅ Always implement __repr__
# __str__ falls back to __repr__ if not defined
class GoodClass:
def __repr__(self):
return f'{self.__class__.__name__}({self.__dict__})'
# This gives useful debugging output
gc = GoodClass()
print(repr(gc)) # GoodClass({})
Format Specification
class Color:
def __init__(self, r, g, b):
self.r = r
self.g = g
self.b = b
def __format__(self, format_spec):
if format_spec == 'hex':
return f'#{self.r:02x}{self.g:02x}{self.b:02x}'
elif format_spec == 'rgb':
return f'rgb({self.r}, {self.g}, {self.b})'
return f'Color({self.r}, {self.g}, {self.b})'
c = Color(255, 128, 0)
print(f'{c}') # Color(255, 128, 0)
print(f'{c:hex}') # #ff8000
print(f'{c:rgb}') # rgb(255, 128, 0)
Operator Overloading
Arithmetic Operators
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other): # self + other
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other): # self - other
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar): # self * other
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar): # other * self (reflected)
return self.__mul__(scalar)
def __truediv__(self, scalar): # self / other
return Vector(self.x / scalar, self.y / scalar)
def __neg__(self): # -self
return Vector(-self.x, -self.y)
def __abs__(self): # abs(self)
return (self.x**2 + self.y**2)**0.5
def __repr__(self):
return f'Vector({self.x}, {self.y})'
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v1 - v2) # Vector(-2, -2)
print(v1 * 3) # Vector(3, 6)
print(3 * v1) # Vector(3, 6) - uses __rmul__
print(v1 / 2) # Vector(0.5, 1.0)
print(-v1) # Vector(-1, -2)
print(abs(v1)) # 2.236...
Comparison Operators
class Money:
def __init__(self, amount, currency='USD'):
self.amount = amount
self.currency = currency
def __eq__(self, other): # self == other
return self.amount == other.amount and self.currency == other.currency
def __lt__(self, other): # self < other
if self.currency != other.currency:
raise ValueError('Cannot compare different currencies')
return self.amount < other.amount
def __le__(self, other): # self <= other
return self == other or self < other
def __gt__(self, other): # self > other
return not self <= other
def __ge__(self, other): # self >= other
return not self < other
def __hash__(self):
return hash((self.amount, self.currency))
def __repr__(self):
return f'Money({self.amount}, {self.currency}')'
m1 = Money(100, 'USD')
m2 = Money(200, 'USD')
print(m1 < m2) # True
print(m1 == m2) # False
print(m1 >= m1) # True
Container Methods
class Playlist:
def __init__(self, name):
self.name = name
self._songs = []
def __len__(self): # len(self)
return len(self._songs)
def __getitem__(self, index): # self[index]
return self._songs[index]
def __setitem__(self, index, value): # self[index] = value
self._songs[index] = value
def __delitem__(self, index): # del self[index]
del self._songs[index]
def __contains__(self, item): # item in self
return item in self._songs
def __iter__(self): # for item in self
return iter(self._songs)
def add(self, song):
self._songs.append(song)
playlist = Playlist('My Songs')
playlist.add('Song 1')
playlist.add('Song 2')
print(len(playlist)) # 2
print(playlist[0]) # Song 1
print('Song 1' in playlist) # True
for song in playlist:
print(song)
Context Managers & Callables
Context Manager Protocol
class Timer:
def __init__(self, label):
self.label = label
def __enter__(self):
import time
self.start = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
import time
self.elapsed = time.time() - self.start
print(f'{self.label}: {self.elapsed:.4f} seconds')
return False # Don't suppress exceptions
with Timer('Loop'):
total = sum(range(1000000))
# Loop: 0.0312 seconds
Custom Exception Handling
class ManagedResource:
def __enter__(self):
print('Acquiring resource')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
print(f'Error occurred: {exc_val}')
# Return True to suppress exception
# Return False (or None) to re-raise
print('Releasing resource')
return False
Callable Objects
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, x):
return x * self.factor
double = Multiplier(2)
triple = Multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# Can be used anywhere a function is expected
numbers = [1, 2, 3, 4, 5]
doubled = list(map(double, numbers))
# [2, 4, 6, 8, 10]
# Check if object is callable
print(callable(double)) # True
print(callable(42)) # False
Other Useful Magic Methods
class MyClass:
def __init__(self, value):
self.value = value
def __hash__(self): # hash(self)
return hash(self.value)
def __bool__(self): # bool(self)
return bool(self.value)
def __sizeof__(self): # sys.getsizeof(self)
return object.__sizeof__(self) + 8
def __copy__(self): # copy.copy(self)
return self.__class__(self.value)
def __deepcopy__(self, memo): # copy.deepcopy(self)
return self.__class__(self.value)
# Usage
obj = MyClass(42)
print(hash(obj)) # 42
print(bool(obj)) # True
print(bool(MyClass(0))) # False