Bundling Data With Behaviour
A dictionary can hold a name and an age, but nothing stops you misspelling the key next time you use it. Define a class once, stamp out three objects from it, and give each its own values without repeating the shape.
Read first: Looking Things Up Instead of Counting Along
A dictionary can hold a name and a grade, but nothing stops you misspelling the key next time you use it. Define a class once, stamp out three objects from it, and give each its own values without repeating the shape.
A blueprint, and the objects made from it
class Student: defines a shape, not a value. Nothing exists yet until you call it like a function — Student("Ada", 92) — which creates one actual object, called an instance, following that shape.
The class itself is not a student — it is the plan for one. Stamp out three instances from it and you get three separate objects in memory, each following the same plan, and changing one of them never touches the other two, or the class they both came from.
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = gradeNo objects yet — the class itself is just a blueprint. Click the button to stamp one out.
self is the object talking about itself
Inside the class, self refers to whichever instance is currently being worked on. self.name = name means “store this particular call's name on this particular object”, not on the class itself.
__init__ is often called “the constructor”, which is close enough to be useful and wrong enough to trip you up later. By the time __init__ runs, the object already exists — Python has already allocated it. __init__'s job is narrower: it initialises that already-existing object with starting values. The distinction rarely matters until you meet __new__, the method that actually constructs the object, which almost no everyday Python code ever needs to touch.
$ >>> ada = Student("Ada", 92)$ >>> grace = Student("Grace", 88)$ >>> ada.name$ 'Ada'$ >>> grace.name$ 'Grace'Python passes the object in as self automatically every time you call a method on it — you never pass it yourself. That is the entire reason every method you define takes self as its first parameter.
Bundling data and the functions that act on it
A class can hold functions as well as data, and those functions automatically get access to that object's own values through self.
$ >>> class Student:$ ... def __init__(self, name, grade):$ ... self.name = name$ ... self.grade = grade$ ... def passed(self):$ ... return self.grade >= 60$ ...$ >>> ada = Student("Ada", 92)$ >>> ada.passed()$ TrueAdd a second method and it works the same way — each one takes self first, and each one can read or change anything already stored on that instance. A class with two fields and three methods is not three separate pieces of code that happen to share some data; it is one unit, and that is the entire design goal.
Instance attributes belong to the object; class attributes are shared
Everything so far — self.name, self.grade — is an instance attribute: set inside __init__ through self, and every object gets its own separate copy. A class attribute, written directly inside the class body with no self, works differently — there is exactly one copy, shared by every instance of that class.
Instance attribute
Class attribute
$ >>> class Student:$ ... school = "Lincoln High"$ ... def __init__(self, name):$ ... self.name = name$ ...$ >>> ada = Student("Ada")$ >>> grace = Student("Grace")$ >>> ada.school$ 'Lincoln High'$ >>> grace.school$ 'Lincoln High'That is fine for a value that is the same for everyone. It becomes a bug the moment the shared value is mutable — a list or a dictionary — because changing it through one instance changes it for every instance at once, since there was only ever one list to begin with.
$ >>> class Student:$ ... clubs = []$ ... def __init__(self, name):$ ... self.name = name$ ...$ >>> ada = Student("Ada")$ >>> grace = Student("Grace")$ >>> ada.clubs.append("Chess")$ >>> grace.clubs$ ['Chess']grace.clubs shows a club she never joined, because ada.clubs and grace.clubs were never two lists — clubs lives on the class, not on either instance, so both names point at the same one. The fix is to create the mutable value inside __init__ instead, where each call makes a new one.
A repr that helps with debugging
Print a plain object and Python shows you something like <__main__.Student object at 0x104a3f550> — technically correct, and useless for debugging, because it tells you nothing about which student it actually is.
$ >>> ada = Student("Ada", 92)$ >>> print(ada)$ <__main__.Student object at 0x104a3f550>Define __repr__ and that changes: whatever string it returns is what print, and the console, show instead.
$ >>> class Student:$ ... def __init__(self, name, grade):$ ... self.name = name$ ... self.grade = grade$ ... def __repr__(self):$ ... return f"Student({self.name!r}, {self.grade})"$ ...$ >>> ada = Student("Ada", 92)$ >>> print(ada)$ Student('Ada', 92)$ >>> ada$ Student('Ada', 92)Define it on almost every class you write, not just the ones you plan to print on purpose. When something goes wrong three functions away and you inspect a variable in a debugger or an error message, __repr__ is what you actually read.
Not everything needs to be a class
A class earns its place when a group of values travel together and share behaviour that acts on them — a student's name, grade, and the passed() method that reads both. A single function that takes an argument and returns a value does not need a class wrapped around it just because the rest of the file has some.
# no class needed — one input, one output, no state to bundledef average(grades): return sum(grades) / len(grades)Signs a class is not pulling its weight
- →Exactly one method besides __init__, and it barely touches self.
- →Every instance is created, used once, and thrown away immediately.
- →You keep writing Thing(x).run() where run(x) would do the same job in one line.
Writing the plain function instead is not a shortcut taken to skip “proper” object-oriented code — for that shape of problem, it is already the more direct solution.
Key takeaways
- class Student: is a blueprint. Calling it like a function — Student("Ada", 92) — creates one instance following that blueprint.
- __init__ initialises an object that already exists; it does not construct it. self is that specific object, passed in automatically on every method call.
- self.x = x inside __init__ gives every instance its own copy. x = ... written directly in the class body creates one value shared by every instance — a bug waiting to happen the moment that value is mutable.
- Define __repr__ so printing an object shows something useful instead of a memory address, especially once you are debugging three functions away from where the object was created.
- A class earns its place when data and the behaviour that acts on it travel together. A single function that takes an input and returns an output rarely needs one.
Quick check
Answer these to unlock the next chapter — 3 of 4 to pass. You can retake it anytime.
Answer every question to check.
Make a free account to read on
Every chapter is free — an account is how your progress, XP, and streak follow you from your laptop to your phone, and how you show up on the leaderboard. No payment, no trial.