Skip to content
wannacode

Behavior

Problem

Everyone withdraws their own way

The bank has an Account class with an owner and a balance. Money is withdrawn in two places in the program. The ATM checks that there is enough money. The card payment was written by someone else, and it does not check:

account = Account("Maria", 100)
# The ATM checks that there is enough money.
if account.balance >= 70:
account.balance -= 70
# Someone else wrote the card payment. It has no check.
account.balance -= 50
print(account.balance)
Output
-20

The account is below zero. The rule "you cannot withdraw more than you have" is important, but it is written (or forgotten) separately in every place that withdraws.

The idea

Methods live with the data

The withdrawal logic is part of the account, so put it in the Account class. A function written in the class is a method of the object:

bank.py
class Account:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount <= self.balance:
self.balance -= amount
else:
print(f"Not enough money: {self.owner}")
account = Account("Maria", 100)
account.withdraw(70)
account.withdraw(50)
account.deposit(20)
print(account.balance)
Output
Not enough money: Maria
50
  • The method is a def indented inside the class. Its first parameter is self, as with __init__.
  • You call it on an object with a dot: account.withdraw(70), just like the fields. Python puts account into self by itself, so you pass only the amount.
  • Inside, self.balance is the balance of this account, the object before the dot. The self. is required: without it, balance is not the field.
  • The check is written once. Anyone who withdraws through the method goes through it.

maria = Account("Maria", 100)
peter = Account("Peter", 40)
maria.withdraw(70)
peter.withdraw(70)
print(maria.balance, peter.balance)
Step by step, in words
  1. Two accounts, two objects. Each one has its own balance.
  2. maria.withdraw(70): self gets an arrow to the object before the dot.
  3. In the method, self.balance is Maria's balance: 100 - 70 = 30.
  4. Same method, different object: now self points to Peter.
  5. 70 is more than 40: the method prints a refusal, and Peter's balance stays the same.
  6. There is one copy of the method code. The fields belong to the object you call it on.

The card payment can no longer "forget" the check if it withdraws with withdraw. It can still write straight into the field, though. You will see how to stop that in the "Encapsulation" lesson.

The idea

Method or function

The functions from the "Functions" module live outside the class. They get everything as parameters and have no object of their own. A method works with the fields of the object it was called on:

def apply_discount(price, percent):
return price * (100 - percent) / 100
price = apply_discount(10, 10)
account.withdraw(70)

You cannot call an instance method through the class name. It is not clear which account to withdraw from:

Account.withdraw(70)
Error
TypeError: Account.withdraw() missing 1 required positional argument: 'amount'

70 went into self, and nothing was left for amount. In fact, account.withdraw(70) is short for Account.withdraw(account, 70): the object before the dot becomes the first argument. There are also methods of the class itself, without an object. You will see them in the lesson "What all objects share: class attributes".

Try it

Whose account?

What will the program print?

maria = Account("Maria", 100)
peter = Account("Peter", 40)
maria.deposit(30)
peter.withdraw(10)
print(maria.balance, peter.balance)
Answer

Try it

Find the bug

account.withdraw(30) stops the program with an error. Which line has the bug?

Pick the line with the bug.

The idea

A method can also answer

An instance method can return a result, just like the functions from the "Functions" module. A product knows its price and quantity, so it can tell you its own cost:

class Product:
# the fields and the constructor, as before
def total(self):
return self.price * self.quantity
cart = [
Product("Bread", 1.20, 2),
Product("Milk", 2.10, 1),
Product("Coffee", 7.50, 1),
]
amount = 0
for p in cart:
amount += p.total()
print(f"Total: €{amount}")
Output
Total: €12.0

Compare this with the first lesson of the module: there, the sum was computed in the code below the class from the fields. Now that code only asks each product. If a bulk discount is added tomorrow, you change only total().

The idea

What does the object look like as text?

If you print a product directly, you get something useless:

coffee = Product("Coffee", 7.50, 1)
print(coffee)
Output
<__main__.Product object at 0x7f3a2c1b2d10>

Python prints the class name and the address of the object in memory, which is different on every run. It tells you nothing about the product.

The class can say how it should be shown. The method is called __str__, and it returns text:

class Product:
# the fields and the constructor, as before
def total(self):
return self.price * self.quantity
def __str__(self):
return f"{self.name} x{self.quantity}: €{self.total()}"
coffee = Product("Coffee", 7.50, 1)
bread = Product("Bread", 1.20, 2)
print(coffee)
print(f"In the cart: {bread}")
Output
Coffee x1: €7.5
In the cart: Bread x2: €2.4

You do not call it yourself. print(coffee) and the f-string f"In the cart: {bread}" call it for you when the object has to become text. The two underscores in front and behind mean exactly that: the method is for Python, and Python calls it on its own at the right moment, like __init__ at creation.

With +, though, you can only join text to text. "In the cart: " + bread stops with

Error
TypeError: can only concatenate str (not "Product") to str

Use an f-string or "In the cart: " + str(bread): str() also calls __str__.

More precisely, if you are curious

Besides __str__, there is also __repr__: the text for the programmer. A list uses it when you print the list, so without __repr__, print(cart) still gives <__main__.Product object at …>. By convention, __repr__ looks like the code that creates the object:

class Product:
# the fields and the constructor, as before
def __repr__(self):
return f'Product("{self.name}", {self.price}, {self.quantity})'
cart = [Product("Coffee", 7.50, 1), Product("Bread", 1.20, 2)]
print(cart)
Output
[Product("Coffee", 7.5, 1), Product("Bread", 1.2, 2)]

Try it

A book in a sentence

What will the program print?

class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __str__(self):
return f"{self.title} by {self.author}"
book = Book("Tobacco", "Dimitar Dimov")
print(f"Reading: {book}")
Answer

Exercise

Objects that do something

Solve the tasks in your editor. Take the classes from the earlier lessons and add methods to them.

  1. Person: a method introduce() that prints the name and the age.
  2. SimpleCar: a method accelerate() that increases the speed by 10. Now the code below the class does not touch the speed directly.
  3. DanceMove: a method perform() that "performs" the move.
  4. Book: a method read() that prints "Reading [title] by [author]".

Create one object of each class and call its methods:

Output
Hi, I'm Maria and I'm 34 years old.
Speed after accelerating: 10
Performing moonwalk for 5 seconds
Reading Under the Yoke by Ivan Vazov

Exercise

A car with fuel

A Car class with a brand, a speed, fuel (in liters) and a mileage in kilometers. The constructor gets the brand and the fuel. The speed and the mileage start at 0.

  • accelerate() increases the speed by 10.
  • refuel(liters) adds fuel, but the tank holds at most 50 liters.
  • drive(distance) uses 6 liters per 100 km and adds the distance to the mileage. If there is not enough fuel, it prints a message and the car does not move.
  • __str__() returns the brand, the mileage and the fuel (the fuel with one digit after the point: {self.fuel:.1f}).
The program
car = Car("Toyota", 20)
car.accelerate()
car.accelerate()
print(f"Speed: {car.speed}")
car.drive(100)
print(car)
car.drive(300)
car.refuel(40)
print(car)
car.drive(300)
print(car)
Output
Speed: 20
Toyota: 100 km, 14.0 L
Not enough fuel for 300 km
Toyota: 100 km, 50.0 L
Toyota: 400 km, 32.0 L
Hint: distance * 6 / 100. In Python, / always gives a decimal number, so for 30 km you get 1.8 liters.

More tasks

  • Discount. Add a method apply_discount(percent) to Product that lowers the price of the product. Compare it with the function from the "Functions" module that returned a new price.
  • An account with a history. Add an operation counter to Account: every deposit and every successful withdrawal increases it by 1. A method __str__() returns the owner, the balance and the number of operations.

Test

Check yourself

1. What does maria.deposit(30) do?
2. What can a method see that a function outside the class cannot?
3. Why does Account.withdraw(70) stop with a TypeError?
4. When does Python call __str__ on its own?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·