Attributes and Special Methods
In the previous lesson all of an object's data lived in attributes like self.name — each object had its own. Today we'll look at data shared by all objects of a class, defuse a classic __init__ trap, and teach objects to print and compare like proper values.
Instance attributes and class attributes
The attributes that __init__ creates via self.name = ... are called instance attributes: each object gets its own value. But some data is the same for everyone. Say all our students go to the same school — storing its name in every object makes no sense: the value is one for all, and if it changes, you'd have to update every object.
Such data is declared right in the class body, outside the methods. That's a class attribute:
Python 3.13class Student: school = "School No. 1" # class attribute — one for all def __init__(self, name): self.name = name # instance attribute — each has its own student1 = Student("Alex") student2 = Student("Kate") print(f"{student1.name}, {student1.school}")Alex, School No. 1print(f"{student2.name}, {student2.school}")Kate, School No. 1
Notice: student1.school reads fine even though the object itself has no such attribute. When Python doesn't find the name in the instance, it looks it up in the class. So changing the class attribute is enough — all objects see the change at once:
Python 3.13class Student: school = "School No. 1" def __init__(self, name): self.name = name student1 = Student("Alex") student2 = Student("Kate") Student.school = "School No. 5" print(f"{student1.name}, {student1.school}")Alex, School No. 5print(f"{student2.name}, {student2.school}")Kate, School No. 5
name — each object has its own, while school lives in the class: one for allThe rule of thumb is simple: whatever differs between objects goes into instance attributes (name, age), and whatever is one for all goes into class attributes (constants, shared settings, default values).
The mutable default value trap
This __init__ trap catches even experienced developers. Say we want a student to have an empty list of grades by default. It seems logical to write this:
Python 3.13class Student: def __init__(self, name, grades=[]): # looks harmless self.name = name self.grades = grades s1 = Student("Anna") s1.grades.append(5) print(s1.grades)[5]s2 = Student("Ivan") print(s2.grades) # expecting [][5]
Ivan ended up with Anna's grade, even though we added nothing to him. Why?
Default parameter values are evaluated once, at the moment the function is defined — not on every call. The list [] was created once and is reused by every Student(...) call without a grades argument. In other words, s1.grades and s2.grades point to the very same list in memory.
The correct pattern: use None as the default — the technique you know from the previous lesson — and create the real list inside:
Python 3.13class Student: def __init__(self, name, grades=None): self.name = name if grades is None: grades = [] self.grades = grades s1 = Student("Anna") s1.grades.append(5) print(s1.grades)[5]s2 = Student("Ivan") print(s2.grades)[]
Now every object gets its own empty list. The same trick works for dictionaries, sets and any other mutable values.
Special methods
The data is neatly stored in attributes — now let's see how the object behaves in everyday operations. Print it and compare two identical ones:
Python 3.13class Vector: def __init__(self, x, y): self.x = x self.y = y v1 = Vector(3, 4) v3 = Vector(3, 4) print(v1)<__main__.Vector object at 0x7f9b1c2d3e50>print(v1 == v3)False
Printing produced the object's memory address instead of its contents, and the comparison answered False despite equal coordinates: without our help, Python compares objects by whether they are the same object in memory, not by content.
Both problems are solved with special methods — methods with double underscores in the name that Python calls itself at the right moment:
- __init__ you already write in every class: it fires on Student(...);
- __str__ fires when the object needs to become a string — inside print(), for example;
- __eq__ — on comparison with ==;
- __add__ — on addition with +.
Let's define the three new ones in Vector:
Python 3.13class Vector: def __init__(self, x, y): self.x = x self.y = y def __str__(self): return f"Vector({self.x}, {self.y})" def __add__(self, other): return Vector(self.x + other.x, self.y + other.y) def __eq__(self, other): return self.x == other.x and self.y == other.y v1 = Vector(3, 4) v2 = Vector(1, 2) v3 = Vector(3, 4) print(v1)Vector(3, 4)print(v1 + v2)Vector(4, 6)print(v1 == v3)True
Note __add__: it doesn't change the original vectors — it builds a new object out of their coordinates and returns it.
Python has dozens of special methods: __len__ teaches an object to answer len(...), __repr__ — to show itself while debugging. No need to memorize the list — they'll come up throughout the course as needed.
Understanding check
The program created two students. What does the last line print?
Python 3.13class Student: def __init__(self, name, grades=[]): self.name = name self.grades = grades s1 = Student("Anna") s1.grades.append(5) s2 = Student("Ivan") print(s2.grades)
In the next lesson we'll look at inheritance: how one class continues another, reusing its attributes and methods.
