Magic Methods: __str__, __len__, __eq__
Master Python magic methods (dunder methods): __str__, __repr__, __len__, __eq__, __hash__, __contains__, __iter__, and how they make custom classes feel native.
What are Magic Methods?
Magic methods (also called dunder methods ā "double underscore") let your custom classes hook into Python's built-in syntax and protocols. When you write print(obj), Python calls obj.__str__(). When you write len(obj), it calls obj.__len__().
They make custom classes feel like native Python types.
__str__ and __repr__
class Drug:
def __init__(self, name: str, category: str, dose_mg: float):
self.name = name
self.category = category
self.dose_mg = dose_mg
def __str__(self) -> str:
"""Human-readable: called by print() and str()."""
return f"{self.name} ({self.category}, {self.dose_mg}mg)"
def __repr__(self) -> str:
"""Developer-facing: called in REPL, debuggers, and repr()."""
return f"Drug(name={self.name!r}, category={self.category!r}, dose_mg={self.dose_mg})"
warfarin = Drug("warfarin", "anticoagulant", 5.0)
print(warfarin) # "warfarin (anticoagulant, 5.0mg)" ā uses __str__
print(repr(warfarin)) # "Drug(name='warfarin', category='anticoagulant', dose_mg=5.0)" ā uses __repr__
print([warfarin]) # [Drug(name='warfarin', ...)] ā list repr uses __repr__ for elements
# !r in f-string: apply repr() to the value (adds quotes around strings)
name = "warfarin"
print(f"{name}") # warfarin
print(f"{name!r}") # 'warfarin'Rule: __repr__ should be unambiguous and ideally valid Python to recreate the object. __str__ should be readable for end users.
__len__ and __bool__
class DrugFormulary:
def __init__(self):
self._drugs: dict[str, Drug] = {}
def add(self, drug: Drug) -> None:
self._drugs[drug.name.lower()] = drug
def __len__(self) -> int:
"""Called by len(formulary)."""
return len(self._drugs)
def __bool__(self) -> bool:
"""Called by if formulary: ā True if non-empty."""
return bool(self._drugs) # Or: return len(self) > 0
formulary = DrugFormulary()
print(len(formulary)) # 0
if not formulary:
print("Formulary is empty")
formulary.add(warfarin)
print(len(formulary)) # 1
if formulary:
print("Formulary has drugs")__eq__ and __hash__
class DrugInteraction:
def __init__(self, drug_a: str, drug_b: str, severity: str):
# Store in canonical order so (warfarin, aspirin) == (aspirin, warfarin)
self.pair = tuple(sorted([drug_a.lower(), drug_b.lower()]))
self.severity = severity
def __eq__(self, other) -> bool:
"""Called by ==."""
if not isinstance(other, DrugInteraction):
return NotImplemented # Let Python try the other side
return self.pair == other.pair
def __hash__(self) -> int:
"""Required for use in sets or as dict keys ā must be consistent with __eq__."""
return hash(self.pair) # Same pair ā same hash
def __repr__(self) -> str:
return f"DrugInteraction({self.pair[0]!r}, {self.pair[1]!r}, {self.severity!r})"
# Now interactions can be compared and stored in sets
i1 = DrugInteraction("warfarin", "aspirin", "Major")
i2 = DrugInteraction("aspirin", "warfarin", "Major") # Same pair, different order
i3 = DrugInteraction("warfarin", "metformin", "Minor")
print(i1 == i2) # True ā canonical order makes them equal
print(i1 == i3) # False
interaction_set = {i1, i2, i3}
print(len(interaction_set)) # 2 ā i1 and i2 are the sameRule: If you define __eq__, you must define __hash__ if you want objects to be hashable (usable in sets or as dict keys). When __eq__ is defined without __hash__, Python sets __hash__ = None, making the class unhashable.
__contains__: The in Operator
class DrugFormulary:
def __init__(self):
self._drugs: dict[str, Drug] = {}
def add(self, drug: Drug) -> None:
self._drugs[drug.name.lower()] = drug
def __contains__(self, drug_name: str) -> bool:
"""Called by: drug_name in formulary."""
return drug_name.lower() in self._drugs
formulary = DrugFormulary()
formulary.add(warfarin)
print("warfarin" in formulary) # True
print("WARFARIN" in formulary) # True ā normalized
print("penicillin" in formulary) # False__iter__ and __next__: Making Objects Iterable
class DrugBatch:
"""An iterable collection of drugs."""
def __init__(self, drugs: list[Drug]):
self._drugs = drugs
self._index = 0
def __iter__(self):
"""Called at the start of a for loop."""
self._index = 0
return self # Return iterator (self in this case)
def __next__(self) -> Drug:
"""Called on each iteration step."""
if self._index >= len(self._drugs):
raise StopIteration # Signals end of iteration
drug = self._drugs[self._index]
self._index += 1
return drug
def __len__(self) -> int:
return len(self._drugs)
batch = DrugBatch([warfarin, aspirin])
for drug in batch:
print(drug)
# Can also convert to list
print(list(batch)) # [warfarin (anticoagulant, 5.0mg), ...]
# Simpler alternative: just implement __iter__ as a generator
class SimpleDrugBatch:
def __init__(self, drugs: list[Drug]):
self._drugs = drugs
def __iter__(self):
yield from self._drugs # Generator handles __next__ automatically__getitem__ and __setitem__: Index Access
class DrugFormulary:
def __init__(self):
self._drugs: dict[str, Drug] = {}
def add(self, drug: Drug) -> None:
self._drugs[drug.name.lower()] = drug
def __getitem__(self, name: str) -> Drug:
"""Called by formulary["warfarin"]."""
key = name.lower()
if key not in self._drugs:
raise KeyError(f"Drug '{name}' not in formulary")
return self._drugs[key]
def __setitem__(self, name: str, drug: Drug) -> None:
"""Called by formulary["warfarin"] = drug."""
self._drugs[name.lower()] = drug
def __delitem__(self, name: str) -> None:
"""Called by del formulary["warfarin"]."""
del self._drugs[name.lower()]
formulary = DrugFormulary()
formulary["warfarin"] = warfarin # __setitem__
print(formulary["warfarin"]) # __getitem__
del formulary["warfarin"] # __delitem__Arithmetic Operators
from dataclasses import dataclass
@dataclass
class Score:
value: float
def __add__(self, other: "Score") -> "Score":
return Score(self.value + other.value)
def __mul__(self, factor: float) -> "Score":
return Score(self.value * factor)
def __lt__(self, other: "Score") -> bool:
return self.value < other.value
def __repr__(self) -> str:
return f"Score({self.value:.3f})"
s1 = Score(0.85)
s2 = Score(0.72)
print(s1 + s2) # Score(1.570)
print(s1 * 1.1) # Score(0.935)
print(s1 < s2) # False
print(sorted([s2, s1])) # [Score(0.720), Score(0.850)] ā uses __lt__Common Magic Methods Reference
| Method | Triggered by | Return type |
|---|---|---|
| __str__ | print(obj), str(obj) | str |
| __repr__ | repr(obj), REPL display | str |
| __len__ | len(obj) | int |
| __bool__ | if obj:, bool(obj) | bool |
| __eq__ | obj == other | bool |
| __hash__ | hash(obj), obj in set | int |
| __contains__ | item in obj | bool |
| __iter__ | for x in obj: | iterator |
| __next__ | next(obj) | item |
| __getitem__ | obj[key] | item |
| __setitem__ | obj[key] = value | None |
| __delitem__ | del obj[key] | None |
| __add__ | obj + other | new obj |
| __lt__ | obj < other | bool |
| __call__ | obj(args) | anything |
__call__: Making Objects Callable
class PromptFormatter:
def __init__(self, template: str):
self.template = template
def __call__(self, **kwargs) -> str:
"""Makes formatter callable: formatter(drug="warfarin")"""
return self.template.format(**kwargs)
clinical_prompt = PromptFormatter(
"You are a clinical pharmacist. Answer this question about {drug}: {question}"
)
# Use like a function
prompt = clinical_prompt(drug="warfarin", question="What are the main interactions?")
print(prompt)
# "You are a clinical pharmacist. Answer this question about warfarin: What are the main interactions?"This is how LangChain's Runnable protocol works ā .invoke() and | (pipe) are made possible by magic methods under the hood.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.