Classes & the Object Model
Lesson 1 of 5
1Classes & the Object Model
The big pictureBuilding your own type, how a class differs from an object, and what __init__ and self actually do in memory.
What the picture shows: From the Counter template we make a, then b, each with its own count. When we call a.tick() only a changes and b stays at zero.
Map of this part
The concepts in order, click any one to jump to it:
Two ways into the same idea
A class is like a cookie cutter, and an object is a cookie made from it. There is one cutter, but each cookie has its own filling. In the cutter you define what every cookie has (colour, size) and what it can do, then you make as many cookies as you like.
class defines a new type, __init__ sets up an object's state when it is created, and self refers to the current object so you store attributes on it as self.name. Functions inside the class are methods that work on their object's state, and each object in memory keeps its own attributes.
Before You Start
🧩Functions
You define a function with inputs and return a value.
📚The dictionary
Named data together, the closest thing to an object before you learn one.
The Core Idea
A class is a template you make similar things from, each with its own data. Before words like "object" and "attribute", make two students and watch what happens.
1One template, two things from it
class defines the template. __init__ is a function that runs automatically when a new thing is made, and self is that very thing. self.name = name stores the name inside it.
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
first = Student("Sara", 95)
second = Student("Khalid", 80)
print(first.name, first.grade)
print(second.name, second.grade)
We made two students from one template, each with its own name and grade. That is the difference between a class (the template) and an object (a thing made from it).
2A function that lives inside the class
A function inside a class is called a method, and its first input is always self so it can reach the object's data. You call it with a dot: object, dot, method name.
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def passed(self):
return self.grade >= 60
s = Student("Noura", 58)
print(s.passed())
s.grade = 70
print(s.passed())
We changed the grade and the answer of passed changed. A method reads the current data at the moment of the call.
3Each object carries its own state
Two counters from the same template. We ticked the first twice and the second once, and they differ. If the data were shared, that could not happen.
class Counter:
def __init__(self):
self.count = 0
def tick(self):
self.count += 1
a = Counter()
b = Counter()
a.tick()
a.tick()
b.tick()
print(a.count, b.count)
This is the idea everything after it is built on: data and behaviour together in one unit, and every instance with its own state.
self out of a method definition, as in def passed():. On the call s.passed() Python passes the object automatically as the first input, and you get a message saying the function received an input it did not expect. The fix is that self is always the first input.How It Works, Step by Step: What happens on the line s = Student("Sara", 95)
Python makes an empty object of the class Student.
It calls __init__ and passes the object itself as self.
self.name = name stores the value inside this object alone.
The finished object comes back and is bound to the name s.
Going deeper
In Python, everything is an object: numbers, strings, and lists are all instances of predefined classes. This part teaches you how to build your own classes.
- owner: str
- __balance: float (private)
+ deposit(amount)
+ withdraw(amount)
+ __str__()
One class ← an unlimited number of objects (different bank accounts), each with its own independent state.
__init__
The method called automatically when a new object is created; it initializes its starting attributes.
Encapsulation
Hiding internal details from the outside (with a single underscore _ or a double one __) and only allowing access through controlled methods that validate the value. A single underscore is just an implicit convention among programmers ("don't touch this from outside") and doesn't actually block access; a double underscore triggers name mangling: Python internally renames __balance to _BankAccount__balance, so accessing it by its direct name from outside fails, not true privacy like in other languages, but a deliberate obstacle sturdy enough to deter accidental access.
__str__ vs. __repr__
__str__ is a "friendly" text for the end user (used by print()). __repr__ is a precise text for developers, ideally one that could recreate the object.
class BankAccount:
"""Represents a simple bank account with a protected balance."""
def __init__(self, owner, balance=0):
self.owner = owner
self.__balance = balance # private: can't be edited directly from outside
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.__balance += amount
def withdraw(self, amount):
if amount > self.__balance:
raise ValueError("Insufficient balance")
self.__balance -= amount
def __str__(self):
return f"{self.owner}'s account: {self.__balance} SAR"
acc = BankAccount("Sara", 100)
acc.deposit(50)
print(acc) # calls __str__ automatically -> "Sara's account: 150 SAR"
# acc.__balance # doesn't work directly from outside by this name, due to encapsulation
self.balance were public, any external code could set it directly to a negative value (acc.balance = -500) without any check. Encapsulation forces every change to pass through a method that validates the logic first.
Check your understanding: when do you need a separate __repr__ from __str__?
When you want different text for two different purposes: __str__ for a friendly user-facing display (like a receipt), and __repr__ for a precise technical display during debugging that shows all internal values exactly, like BankAccount(owner='Sara', balance=150).
Worked example
Account class with self._balance = 0 and a deposit(self, amount) method that raises ValueError when the amount is zero or less and adds it otherwise. Every change goes through one door that checks it.Myth vs Reality
| Myth | Reality |
|---|---|
| A class and an object are two names for one thing. | The class is the template and the object is a copy made from it. From one class you make as many objects as you like, each with its own data. |
self is a magic reserved word. | It is just the name of the first input, and Python passes the object into it automatically. The name is a convention, not a rule. |
__balance with two underscores is fully private. | Python only renames it internally to make accidental access harder. It is a respected agreement, not a lock. |
Try it yourself
You make a = Counter() and b = Counter(), then call a.tick() three times. What is b.count?
Each object carries its own count, created in __init__. Ticking a never touches b, so it stays at zero as it started.
Quick Review Flashcards
Check Your Understanding
Two objects of one class: do they share self.count?
No, each object has its own copy, and changing one does not touch the other.
What does print(obj) call?
The __str__ method if there is one, otherwise __repr__.
Why wrap the balance instead of leaving it public?
So every change goes through a method that checks it, and one line from outside cannot make the balance negative.
Resources for this part
Sorted by kind: start with the official docs when you want the precise reference, or with an article or video when you want the explanation first.
Resources for this lesson
Grouped by type. Start with the official docs for precision, or an article or video for the explanation.
Official docs3
Done with this lesson?
Sign in to save your progress.