Skip to content
VLSI Mentor

Python Part I · Module 16 · Object-Oriented Programming

Special Methods and __str__

Print one of your own objects and Python tells you its class and where it happens to be sitting in memory — a number that is different every time the program runs and useful to nobody. Fixing that takes one method, and the method is the first example of a much larger idea: there is a set of names Python looks for on your class and calls on your behalf, and they are how len(), ==, print() and the for loop actually work. The obvious fix is also the slightly wrong one, which is worth finding out here rather than in a debugger.

Intermediate16 min readPythonPart IFundamentalsOOPSpecial Methods

Part I · Module 16 · Chapter 16.8 · Special Methods and str

1. The Default Is Useless

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class TestRun:
    def __init__(self, name, seed):
        self.name = name
        self.seed = seed


run = TestRun("smoke", 1234)
text = str(run)

print("class name    :", type(run).__name__)
print("prefix        :", text.split(" object at ")[0])
print("has an address:", " object at 0x" in text)
print("length of text:", len(text) > 20)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class name    : TestRun
prefix        : <__main__.TestRun
has an address: True
length of text: True

The full text is <__main__.TestRun object at 0x...> where 0x... is a memory address. It is not printed here, for a reason that is itself the argument: that number is different every time the program runs, so it cannot appear in a tutorial whose outputs are verified — and by exactly the same token it cannot help you compare two runs of your own script.

What you wanted to know was smoke and 1234. Neither is there.

2. A Name Python Looks For

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class TestRun:
    def __init__(self, name, seed):
        self.name = name
        self.seed = seed

    def __str__(self):
        return f"{self.name} seed={self.seed}"


run = TestRun("smoke", 1234)
print(run)
print("str() :", str(run))
print("f-str :", f"{run}")
print("called explicitly:", run.__str__())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke seed=1234
str() : smoke seed=1234
f-str : smoke seed=1234
called explicitly: smoke seed=1234

print(run) calls str(run), which calls run.__str__(). You never call it directly; Python does it for you when a value needs to become text.

This is the idea the canonical purpose calls "methods Python calls for you". The double underscores mark these names — the same convention as __init__ from Chapter 16.3 — and they are collectively known as special methods or dunder methods.

3. __str__ Does Not Fix Everything

Now the measurement. Define only __str__ and look at the other places an object appears:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class TestRun:
    def __init__(self, name, seed):
        self.name = name
        self.seed = seed

    def __str__(self):
        return f"{self.name} seed={self.seed}"


run = TestRun("smoke", 1234)

print("print()          :", run)
print("repr() has addr  :", " object at 0x" in repr(run))
print("in a list        :", " object at 0x" in str([run]))
print("in a dict        :", " object at 0x" in str({"a": run}))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print()          : smoke seed=1234
repr() has addr  : True
in a list        : True
in a dict        : True

Three of the four are still showing an address. Containers do not use __str__ on their contents — they use __repr__, and so does the interactive interpreter, and so does every debugger.

A list of results is exactly how you look at these objects in practice, so __str__ alone fixes the least important case.

4. Define __repr__ Instead

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class TestRun:
    def __init__(self, name, seed):
        self.name = name
        self.seed = seed

    def __repr__(self):
        return f"TestRun(name={self.name!r}, seed={self.seed})"


run = TestRun("smoke", 1234)

print("print()        :", run)
print("repr()         :", repr(run))
print("in a list      :", [run])
print("any address?   :", " object at 0x" in str([run, run]))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print()        : TestRun(name='smoke', seed=1234)
repr()         : TestRun(name='smoke', seed=1234)
in a list      : [TestRun(name='smoke', seed=1234)]
any address?   : False

One method fixed all four. str() falls back to __repr__ when no __str__ exists, so defining __repr__ covers printing, containers, the debugger and the interpreter.

Define __repr__ first. Add __str__ only when you want a separate, friendlier form for end users.

The convention for a good __repr__ is to look like the call that would recreate the object — TestRun(name='smoke', seed=1234) — which is why !r is used on the string field, so the quotes appear.

When both exist they serve different audiences:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class TestRun:
    def __init__(self, name, seed):
        self.name = name
        self.seed = seed

    def __repr__(self):
        return f"TestRun(name={self.name!r}, seed={self.seed})"

    def __str__(self):
        return f"{self.name} (seed {self.seed})"


run = TestRun("smoke", 1234)
print("for a person :", run)
print("for a developer:", repr(run))
print("in a list      :", [run])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for a person : smoke (seed 1234)
for a developer: TestRun(name='smoke', seed=1234)
in a list      : [TestRun(name='smoke', seed=1234)]

5. Other Names Python Looks For

The same mechanism drives much of the language:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Suite:
    def __init__(self, tests):
        self._tests = tests

    def __repr__(self):
        return f"Suite({self._tests!r})"

    def __len__(self):
        return len(self._tests)

    def __eq__(self, other):
        return self._tests == other._tests


a = Suite(["smoke", "cdc"])
b = Suite(["smoke", "cdc"])
empty = Suite([])

print("len()      :", len(a))
print("==         :", a == b)
print("truthiness :", bool(a), bool(empty))
print("repr       :", a)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
len()      : 2
==         : True
truthiness : True False
repr       : Suite(['smoke', 'cdc'])

Three things to notice.

len(a) called __len__. len is not a method you call on the object; it is a function that looks for __len__.

a == b called __eq__. Without it, Chapter 16.2 Section 8 showed, two objects with identical contents compare False.

Truthiness came for free. bool(empty) is False because Python falls back to __len__ when there is no __bool__ — so an empty Suite is falsy exactly like an empty list, and Chapter 9.5's rules apply to your class without further work.

Others worth knowing by name: __iter__ makes an object usable in a for loop, __getitem__ makes obj[k] work, __enter__/__exit__ make it usable with with (Chapter 15.5), and __add__ makes + work. The point is not the list — it is that each is a name Python looks for, so adding one makes your class work with syntax you did not write.

6. The __eq__ Trap

Defining __eq__ has a consequence that catches everyone once:

Azvya Education Pvt. Ltd.VLSI Mentor
unhashable.py (BROKEN ON PURPOSE)
class Suite:
    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        return self.name == other.name


a = Suite("smoke")
b = Suite("smoke")

print("equal:", a == b)
print("now put one in a set:")
print({a})
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
equal: True
now put one in a set:
Traceback (most recent call last):
  File "unhashable.py", line 14, in <module>
    print({a})
          ^^^
TypeError: unhashable type: 'Suite'

Defining __eq__ removes the default __hash__, so instances can no longer go in a set or be used as dictionary keys (Chapter 7.3's requirement that set members be hashable).

Python does this deliberately: two objects that are equal must hash equally, and it cannot guess your rule. The fix is to define __hash__ too — or, far more practically, to let Chapter 16.15's dataclass handle it.

7. Fixing the Hash Trap Properly

Section 6 left the class unusable in a set. The fix is to define __hash__ alongside __eq__, and the rule it must obey is short:

Two objects that compare equal must hash equal. The simplest way to guarantee that is to hash exactly the fields you compare.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Suite:
    def __init__(self, name, tests):
        self.name = name
        self.tests = tests

    def __repr__(self):
        return f"Suite({self.name!r})"

    def __eq__(self, other):
        return self.name == other.name

    def __hash__(self):
        return hash(self.name)


a = Suite("nightly", ["smoke", "cdc"])
b = Suite("nightly", [])

print("equal          :", a == b)
print("hashes agree   :", hash(a) == hash(b))
print("set collapses  :", len({a, b}))
print("usable as a key:", {a: "PASS"}[b])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
equal          : True
hashes agree   : True
set collapses  : 1
usable as a key: PASS

The set holds one entry, because a and b are equal by the rule the class declared — the tests lists differ and the class said they do not matter.

Note what that implies: __eq__ defines what "the same" means for your type, and everything downstream — sets, dictionary keys, in, list.count — obeys it. Choosing to compare on name alone is a design decision with reach.

Two practical consequences worth knowing:

  • Hash only on fields that never change. Mutating a.name after putting a in a set leaves it in the wrong bucket, and it becomes unfindable.
  • A mutable object usually should not be hashable at all, which is why list and dict are not — and why Chapter 16.15's frozen=True is the clean way to get both.

8. The Container Protocol

Three more names turn an object into something that behaves like a collection:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Suite:
    def __init__(self, name, tests):
        self.name = name
        self.tests = tests

    def __repr__(self):
        return f"Suite({self.name!r})"

    def __len__(self):
        return len(self.tests)

    def __contains__(self, item):
        return item in self.tests

    def __getitem__(self, i):
        return self.tests[i]


s = Suite("nightly", ["smoke", "cdc", "burst"])

print("len()      :", len(s))
print("in         :", "smoke" in s, "|", "arb" in s)
print("index      :", s[0], "|", s[-1])
print("slice      :", s[:2])
print("iterates   :", [t for t in s])
print("truthiness :", bool(s), bool(Suite("empty", [])))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
len()      : 3
in         : True | False
index      : smoke | burst
slice      : ['smoke', 'cdc']
iterates   : ['smoke', 'cdc', 'burst']
truthiness : True False

Six behaviours from three methods, and two of them were free.

Slicing worked without any extra code, because s[:2] passes a slice object to __getitem__, which hands it straight to the list.

Iteration worked without __iter__. Python falls back to calling __getitem__ with 0, 1, 2… until it raises IndexError — which the underlying list does. That fallback is why a for loop over this object works at all, and it is worth knowing because it means __getitem__ alone is often enough.

This is the shape behind every collection you have used. A dict supports in, len() and d[k] because it defines these three; your class can join them in nine lines.

9. One Method Makes Objects Sortable

sorted(), min() and max() all need to know which of two objects comes first, and they ask the same way — <, which is __lt__:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Result:
    def __init__(self, name, runtime_s):
        self.name = name
        self.runtime_s = runtime_s

    def __repr__(self):
        return f"{self.name}({self.runtime_s})"

    def __lt__(self, other):
        return self.runtime_s < other.runtime_s


rs = [Result("cdc", 204.1), Result("smoke", 182.4), Result("burst", 199.8)]

print("sorted       :", sorted(rs))
print("min / max    :", min(rs), max(rs))
print("same as key= :", sorted(rs) == sorted(rs, key=lambda r: r.runtime_s))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sorted       : [smoke(182.4), burst(199.8), cdc(204.1)]
min / max    : smoke(182.4) cdc(204.1)
same as key= : True

One method, and three built-in functions started working. sorted, min and max never mention runtime_s; they call < and the class decides what that means.

The last line is the honest comparison: key=lambda r: r.runtime_s achieves the same thing without __lt__ at all. The difference is where the knowledge lives — with __lt__, "results are ordered by runtime" is stated once in the class; with key=, it is restated at every call site, and two call sites can disagree.

Define __lt__ when your type has one obvious natural order. Use key= when the ordering is a property of the question being asked rather than of the type — sorting the same results by name in one report and by runtime in another.

10. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Bare:
    def __init__(self, n):
        self.n = n


class WithStr:
    def __init__(self, n):
        self.n = n

    def __str__(self):
        return f"WithStr n={self.n}"


class WithRepr:
    def __init__(self, n):
        self.n = n

    def __repr__(self):
        return f"WithRepr(n={self.n})"


for obj in (Bare(1), WithStr(2), WithRepr(3)):
    name = type(obj).__name__
    print(f"{name:<9} print-addr={' object at 0x' in str(obj):<5} "
          f"list-addr={' object at 0x' in str([obj])}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Bare      print-addr=1     list-addr=True
WithStr   print-addr=0     list-addr=True
WithRepr  print-addr=0     list-addr=False

The table is the chapter. Bare shows an address everywhere (1). WithStr fixed printing only. WithRepr fixed both.

The first column prints 1 and 0 while the second prints True and False for the same kind of value — the width specifier :<5 formats the bool as the integer it is, exactly as Chapter 16.1 Section 8 found.

This is also the honest reason the canonical annotation for this chapter teaches __str__: it is the one that makes print(run) work, which is the first thing anyone tries. It is just not the one to stop at.

11. Exercises

Exercise 1 — the default. Create a class with no special methods. Without printing the address, show that str() contains one.

Exercise 2 — __str__. Add __str__ and confirm print, str() and an f-string all use it.

Exercise 3 — the gap. With only __str__ defined, check whether a list of your objects shows an address. Explain which function the list used.

Exercise 4 — __repr__. Replace __str__ with __repr__ and re-run both checks. State the rule in one sentence.

Exercise 5 — both. Define both, with different wording, and show which appears in print, in repr and in a list.

Exercise 6 — __len__ and truthiness. Give a class __len__ and show that an empty instance is falsy without defining __bool__.

Exercise 7 — the hash trap. Define __eq__, then try to put an instance in a set. Record the error and say why Python behaves this way.

Exercise 8 — recreatable repr. Write a __repr__ whose text is a valid call that would rebuild the object. Verify by pasting it back into your program.

12. Summary

  • The default display is the class name and a memory address, which differs on every run and tells you nothing.
  • Special methods are names Python looks for and calls on your behalf — __init__ was the first.
  • __str__ is used by print, str() and f-strings.
  • __repr__ is used by containers, the interpreter and debuggers — and str() falls back to it.
  • Define __repr__ first; a list of objects is how you actually look at them.
  • Add __str__ only for a separate end-user form.
  • A good __repr__ looks like the call that would recreate the object, with !r on string fields.
  • len() calls __len__; == calls __eq__; neither is a method you call directly.
  • __lt__ makes sorted, min and max work — define it when the type has one natural order, and use key= when the order belongs to the question.
  • Truthiness falls back to __len__, so an empty object is falsy for free.
  • Defining __eq__ removes the default __hash__, making instances unusable in sets and as dict keys.
  • Define __hash__ alongside __eq__, hashing exactly the fields you compare — and only fields that never change.
  • __len__, __contains__ and __getitem__ make an object a collection; slicing and iteration then come free, because Python falls back to __getitem__ when there is no __iter__.

The next chapter builds one class on top of another, so shared behaviour is written once.

Where this fits

Part of the Python curriculum.