What you'll learn
- Define a class with
classand create instances by calling it - Give each instance its own state in
__init__usingself - Add a method and understand why
selfis its first parameter - Print an object readably with
__repr__
Concept
A class definition starts with the keyword class and a name in CapWords: class BankAccount:. Inside, indented, go its methods, which are functions that belong to the class. Calling the class like a function, BankAccount("ada", 100), creates a new instance. Each call creates a separate object, so two accounts made from the same class are independent: change one and the other is untouched.

The __init__ method runs automatically when an instance is created, and its job is to set up that instance's state. Its first parameter is self, the new object itself; the rest are whatever the caller passed. Inside, self.owner = owner stores a value on the object as an attribute. From then on account.owner reads it. Attributes are per-instance: they live on the object, not on the class, which is why two accounts can have different balances.
self appears in every method for the same reason. When you call account.deposit(50), Python translates it to BankAccount.deposit(account, 50): the object goes in as the first argument, and the method reaches its own data through it. The name self is a convention, not a keyword, but it is a convention every Python developer follows, and so will you.
| You write | Python runs | self is |
|---|---|---|
BankAccount("ada", 100) |
__init__(new_object, "ada", 100) |
the new object |
account.deposit(50) |
BankAccount.deposit(account, 50) |
account |
account.balance |
read the attribute on account |
not involved |
Creation happens in a fixed order, and knowing it removes most of the confusion around self. Calling BankAccount("ada", 100) does not start with your __init__: Python builds an empty object first, then calls __init__ on that object, passing it in as self with your arguments after it, and only then returns the finished object to you. That is why __init__ returns nothing, and it is why every later call such as account.deposit(50) passes the same object in as self again.

Methods whose names start and end with double underscores are called dunder methods; __init__ is the first one you write and __repr__ is the second, and a later lesson covers the rest.
Code
The smallest useful class. __init__ stores the state, a method changes it, and two instances prove they are independent.
# account.py
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
ada = BankAccount("ada", 100)
linus = BankAccount("linus", 20)
ada.deposit(50)
print(ada.owner, ada.balance)
print(linus.owner, linus.balance)
print(type(ada).__name__, ada is linus)
ada 150
linus 20
BankAccount FalseWhat self is. Calling the method through the class with the object passed explicitly does exactly the same thing, which shows there is no magic in the dot.
# self_explained.py
class Counter:
def __init__(self):
self.count = 0
def tick(self):
self.count += 1
return self.count
c = Counter()
print(c.tick())
print(Counter.tick(c))
print(c.count)
1
2
2Printing an instance gives an unhelpful <__main__.Point object at 0x...> unless you tell Python how to display it. Defining __repr__, a method that returns a string, fixes that, and print and the REPL both use it. Make it look like the call that would recreate the object, NicePoint(1, 2), which is the convention and doubles as documentation.
# repr_demo.py
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
class NicePoint:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"NicePoint({self.x}, {self.y})"
print(str(Point(1, 2)).startswith("<__main__.Point object at"))
print(NicePoint(1, 2))
print([NicePoint(0, 0), NicePoint(3, 4)])
True
NicePoint(1, 2)
[NicePoint(0, 0), NicePoint(3, 4)]Methods that return values and methods that use other methods. withdraw reuses the check in can_withdraw, and __repr__ keeps every print readable.
# methods.py
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def __repr__(self):
return f"BankAccount({self.owner!r}, {self.balance})"
def can_withdraw(self, amount):
return 0 < amount <= self.balance
def withdraw(self, amount):
if not self.can_withdraw(amount):
return False
self.balance -= amount
return True
acct = BankAccount("ada", 100)
print(acct)
print(acct.withdraw(30), acct)
print(acct.withdraw(500), acct)
print(BankAccount("new"))
BankAccount('ada', 100)
True BankAccount('ada', 70)
False BankAccount('ada', 70)
BankAccount('new', 0)Try it yourself
Create book.py with a Book class holding title, author, and pages, plus a pages_read attribute that starts at 0. Add a read(n) method that advances pages_read without going past pages, an is_finished() method, and a __repr__. Create two books, read different amounts from each, and print both to show they are independent.
Common mistakes
- Forgetting
selfin a method's parameters.def deposit(amount):raises aTypeErrorabout argument counts the first time it is called through an instance. - Forgetting
self.when storing.owner = ownerinside__init__creates a local variable that vanishes;self.owner = ownerstores it on the object. - Calling
__init__yourself. It runs automatically when you call the class;BankAccount("ada", 100)is the whole creation.
What's next
class Name:defines a class; calling it creates an instance, and__init__sets that instance's attributes throughself.- Every method receives the instance as
self;obj.method(x)isClass.method(obj, x). - Define
__repr__so instances print as the call that would recreate them.
You can create objects with state. Next you will control that state: encapsulation, properties, and the difference between instance and class attributes.