Classes and objects
Define your own types with class, give each instance its own state through __init__ and self, and tell instance attributes from class attributes.
What you will learn
- Define a class with __init__ and create instances from it
- Explain what self is and how a method call fills it in
- Add methods that read and change an instance's state
- Tell instance attributes from class attributes, and avoid the shared-list trap
On this page
You have used objects since your first lesson: every string, list and dictionary is one. Now you will build your own kind. The fundamentals lesson on records and objects explains why programs group data with the behaviour that belongs to it. This lesson shows how Python spells that idea.
From a dictionary to a class
A dictionary can hold a record, and for small jobs it is a good choice:
account = {"owner": "Asha", "balance": 100}account["balance"] = account["balance"] + 50print(account)
account["balence"] = 0 # a typo creates a new key, silentlyprint(account){'owner': 'Asha', 'balance': 150}{'owner': 'Asha', 'balance': 150, 'balence': 0}The second assignment is a typo, and Python accepted it. The dictionary has no idea what a bank account should look like, so it now carries a stray balence key. A class lets you write down the shape once, and attach the operations that belong to it.
Your first class
A class is a blueprint. Calling it builds an instance, one object made from that blueprint. The __init__ method runs on each new instance and sets up its starting state.
class Account: def __init__(self, owner, balance): self.owner = owner self.balance = balance
acct = Account("Asha", 100)print(acct.owner)print(acct.balance)print(type(acct))Asha100<class '__main__.Account'>Read the class from the top:
class Account:starts the definition. Class names useCapitalisedWords, as PEP 8 recommends.__init__is a method, which is a function defined inside a class. Python calls it for you when you writeAccount("Asha", 100). The double underscores on both sides are why people call it “dunder init”.self.owner = ownerstores a value on the new instance. Values stored this way are attributes.
What self means
The first parameter of every ordinary method receives the instance the method was called on. By convention it is named self. You never pass it yourself, because Python fills it in. Each instance keeps its own attributes:
class Account: def __init__(self, owner, balance): self.owner = owner self.balance = balance
first = Account("Asha", 100)second = Account("Ben", 20)first.balance = 150print(first.balance)print(second.balance)15020Changing first left second alone, because self was a different object each time.
Adding methods
A method is where behaviour lives. It reads and changes the instance through self:
class Account: def __init__(self, owner, balance): self.owner = owner self.balance = balance
def deposit(self, amount): self.balance = self.balance + amount
def describe(self): return f"{self.owner}: {self.balance}"
acct = Account("Asha", 100)acct.deposit(50)print(acct.describe())Asha: 150acct.deposit(50) is shorthand. Python looks up deposit on the class and passes acct as self. You can write that call out in full, which makes it plain that nothing magical is happening:
class Account: def __init__(self, owner, balance): self.owner = owner self.balance = balance
def deposit(self, amount): self.balance = self.balance + amount
acct = Account("Asha", 100)acct.deposit(50)Account.deposit(acct, 25)print(acct.balance)175Both lines changed the same balance: 100, plus 50, plus 25. You will almost always use the short form, but it helps to know that acct.deposit(50) and Account.deposit(acct, 50) mean the same thing.
__init__ takes parameters like any function, so default values work:
class Account: def __init__(self, owner, balance=0): self.owner = owner self.balance = balance
print(Account("Ben").balance)print(Account("Asha", 100).balance)0100Class attributes
A name assigned directly inside the class body, outside any method, is a class attribute. It belongs to the class and every instance can read it.
class Account: bank_name = "Pybank" open_accounts = 0
def __init__(self, owner): self.owner = owner Account.open_accounts = Account.open_accounts + 1
a = Account("Asha")b = Account("Ben")print(a.bank_name, b.bank_name)print(Account.open_accounts)Pybank Pybank2bank_name is the same for every account, so it belongs on the class. open_accounts is a counter that all instances update together, which is why __init__ writes to it through Account.open_accounts and not through self.
Reading and writing behave differently, and this is the part that surprises people. Reading a.bank_name finds the class attribute when the instance has none of its own. But assigning a.bank_name = ... never changes the class. It creates a new attribute on that one instance, which hides the class attribute for that instance only:
class Account: bank_name = "Pybank"
def __init__(self, owner): self.owner = owner
a = Account("Asha")b = Account("Ben")a.bank_name = "Other Bank"print(a.bank_name)print(b.bank_name)print(Account.bank_name)Other BankPybankPybankCommon pitfalls
Putting a mutable value at class level. A list or dictionary written in the class body is created once and shared by every instance:
class Basket: items = []
def add(self, item): self.items.append(item)
mon = Basket()tue = Basket()mon.add("apple")print(tue.items)['apple']Nobody added anything to tue, yet its list has an apple in it. self.items found no instance attribute, so it fell back to the one list on the class, and append changed that shared list. The fix is to create the list inside __init__, so every instance gets its own:
class Basket: def __init__(self): self.items = []
def add(self, item): self.items.append(item)
mon = Basket()tue = Basket()mon.add("apple")print(tue.items)[]Use class attributes for constants and for counters you update through the class name. Anything that should differ from one instance to the next belongs in __init__.
A typo in an attribute name. Reading a misspelled attribute fails loudly. Writing one succeeds quietly:
class Account: def __init__(self, owner, balance): self.owner = owner self.balance = balance
acct = Account("Asha", 100)acct.balence = 500 # typo: creates a new attributeprint(acct.balance)print(acct.balence)try: print(acct.blance)except AttributeError as error: print("AttributeError:", error)100500AttributeError: 'Account' object has no attribute 'blance'The assignment acct.balence = 500 created a second attribute, and balance stayed at 100. A test that reads the attribute back catches this kind of mistake, and so does a type checker.
Forgetting self. If you write def deposit(amount): without self, calling acct.deposit(50) fails with a TypeError that says the method took 1 positional argument but 2 were given. Python passed the instance as the first argument, and your method had no parameter to receive it. Likewise, inside a method, write self.balance to reach the attribute. A bare balance is a local variable that does not exist.
Forgetting the parentheses. acct.describe is the method itself. acct.describe() calls it.
Checking what something is
isinstance(value, Class) answers whether a value was made from a class, and __class__.__name__ gives the class name as text:
class Account: def __init__(self, owner): self.owner = owner
acct = Account("Asha")print(isinstance(acct, Account))print(isinstance("Asha", Account))print(acct.__class__.__name__)TrueFalseAccountYou met isinstance with built-in types earlier. It works the same way with the classes you write.
Write a class Rectangle whose __init__ takes width and height. Give it an area() method and a perimeter() method, both returning a number.
For Rectangle(3, 4), area() should return 12 and perimeter() should return 14. After you set r.width = 10 on that rectangle, area() should return 40.
Show solution
class Rectangle: def __init__(self, width, height): self.width = width self.height = height
def area(self): return self.width * self.height
def perimeter(self): return 2 * (self.width + self.height)
r = Rectangle(3, 4)print(r.area())print(r.perimeter())r.width = 10print(r.area())121440The methods read self.width and self.height each time they run, which is why they notice the later change to width. If area had stored its answer in __init__, it would have gone stale.
Extend the Account class with these rules. deposit(amount) raises ValueError unless the amount is above zero. withdraw(amount) raises ValueError if the amount is more than the balance. Both change balance otherwise.
Test it with this script: create Account("Asha", 100), deposit 50, try to withdraw 500 and print the error, then withdraw 30 and print the balance. Expected output:
error: cannot withdraw 500, balance is 150120Show solution
class Account: def __init__(self, owner, balance=0): self.owner = owner self.balance = balance
def deposit(self, amount): if amount <= 0: raise ValueError(f"deposit must be positive: {amount}") self.balance = self.balance + amount
def withdraw(self, amount): if amount > self.balance: raise ValueError(f"cannot withdraw {amount}, balance is {self.balance}") self.balance = self.balance - amount
acct = Account("Asha", 100)acct.deposit(50)try: acct.withdraw(500)except ValueError as error: print("error:", error)acct.withdraw(30)print(acct.balance)error: cannot withdraw 500, balance is 150120The checks come before the change to self.balance, so a refused request leaves the account untouched. The messages name the offending values, as the raising exceptions lesson recommends.
Read this program and write down both printed lines before you run it. Say why the last line prints what it does.
class TallyCounter: total = 0
def __init__(self): self.count = 0
def hit(self): self.count = self.count + 1 TallyCounter.total = TallyCounter.total + 1
a = TallyCounter()b = TallyCounter()a.hit()a.hit()b.hit()print(a.count, b.count, TallyCounter.total)print(a.total)Show solution
2 1 33count is an instance attribute: a hit twice and b once. total is a class attribute updated through TallyCounter.total, so all three hits land in the same counter. The last line, a.total, finds no total on the instance and falls back to the class attribute, which is 3.
Summary
- A
classstatement defines a blueprint. Calling the class creates an instance and runs__init__on it. selfis the instance a method was called on. Python passes it for you, soacct.deposit(50)isAccount.deposit(acct, 50).- Attributes set on
selfbelong to one instance. Names assigned in the class body belong to the class and are shared. - Assigning through an instance never changes a class attribute. It creates an instance attribute that hides it.
- Create mutable per-instance values, such as lists, inside
__init__, not in the class body.