Beginner · Lesson 35 of 94

Classes and objects

Watch this lesson

View full playlist ↗

Read the lesson

What you'll learn

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.

Classes and objects: class BankAccount → instance: owner ada, balance 100 → instance: owner linus, balance 20
Classes and objects · Figure 1 · Open for full size

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.

Classes and objects: Diagram 2
Classes and objects · Figure 2 · Open for full size

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)
Output
ada 150
linus 20
BankAccount False

What 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)
Output
1
2
2

Printing 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)])
Output
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"))
Output
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

What's next

You can create objects with state. Next you will control that state: encapsulation, properties, and the difference between instance and class attributes.

Course overview