Classes and objects
A class is a blueprint. The class line and the indented lines under it describe what every bank account will hold and what it can do. Running it makes no account yet, so nothing holds money.
a = BankAccount("Ana") makes a new, empty object and runs __init__ on it. Inside, self is that new object, so self.owner = owner gives a its own owner, and its balance starts at 0.
b = BankAccount("Ben") uses the same blueprint to make a second object, also called an instance. Its owner and balance cells are its own: changing b never touches a.
Methods and encapsulation
a.deposit(50) calls a method: a function that lives on the class and is shared by every account. Python runs it with self = a, so line 6 adds 50 to a's balance only. b still has 0.
Encapsulation: change the balance only through methods, which check the rules. a.withdraw(80) asks for more than the 50 there, so line 9 raises ValueError and nothing changes. The underscore in _balance means: internal, use the methods.
Inheritance
class Savings(BankAccount) makes a subclass: a Savings account is a BankAccount. It writes no __init__, so s = Savings("Sam") reuses the parent's. s gets its own owner, Sam, and a balance of 0.
s.deposit(30): Savings has no deposit method, so Python looks in its parent, BankAccount, and runs that one with self = s. A subclass gets every parent method for free. s now holds 30.
Savings writes its own withdraw, which overrides the parent's. s.withdraw(10) runs the Savings version: it adds a fee of 1, then super() hands 11 to the parent's withdraw. 30 minus 11 leaves 19.
Polymorphism and composition
for acc in (a, s): acc.withdraw(5). The same call reaches two objects, and each runs its own class's withdraw. a loses 5 and has 45. s also pays the fee, loses 6 and has 13. Same name, different behaviour.
A bank is not an account, so it should not inherit from one. It has accounts: bank = Bank([a, b, s]) keeps the three objects in an attribute. Prefer this, composition, when one thing contains or uses another.
Try it in the Python prompt
;; step 1/10 · class BankAccount: >>> class BankAccount: ... ... # the ten lines on the card >>> BankAccount <class '__main__.BankAccount'>
a.deposit(50) is exactly equivalent to BankAccount.deposit(a, 50). That is why every method's first parameter, named self by convention, receives the object the method was called on. Python has no truly private attributes. A leading underscore, as in _balance, is a convention that marks a non-public part: code outside can still reach a._balance, but should not. The cards indent by two spaces so each line fits. Python only needs the indentation to be consistent; the style guide PEP 8 recommends four spaces.