My bank opened the same account twice
See the problem
Section titled “See the problem”You build accounts for a small bank. An Account has an owner, a balance and a log. A Savings account adds interest. An Audited account writes “audit on” to its log. The bank asks for an account that is both, so you write:
class AuditedSavings(Savings, Audited): def __init__(self, owner, balance=0, rate=0.02): Savings.__init__(self, owner, balance, rate) Audited.__init__(self, owner, balance)Every new account is added to the bank’s list, OPENED. You open one for Ana:
>>> AuditedSavings("ana", 100)>>> OPENED['ana', 'ana']The bank now thinks Ana has two accounts. Where did the second one come from?
Define the task
Section titled “Define the task”- Input: an owner, a starting balance, and for savings, an interest rate.
- Output: account objects.
SavingsandAuditedreuseAccount’s code.AuditedSavingsis both, and opening one adds the owner toOPENEDonce. - Also: an account prints as
Account('ana', 100), and two accounts with the same type, owner and balance are equal. - The hard part: with two parents that share a grandparent, each
__init__must still run exactly once.
Example: AuditedSavings("ana", 100) leaves OPENED == ["ana"] and log == ["open ana", "audit on"].
Pause and think
Section titled “Pause and think”Hints open one at a time:
Hint 1 of 3
Each parent calls Account.__init__ itself, so it runs once per parent: twice.
Hint 2 of 3
Python works out one order for all the classes, called the MRO. Look at AuditedSavings.__mro__.
Hint 3 of 3
super() calls the next class in that order, not always the parent you wrote. So each class runs once.
Understand the idea
Section titled “Understand the idea”A class is a template for objects. __init__ runs when you make one and sets its attributes. Methods are functions that get the object as self.
class Account: def __init__(self, owner, balance=0): self.owner = owner # an attribute of this object self.balance = balance
def deposit(self, amount): # a method: a.deposit(5) passes a as self self.balance += amountInheritance reuses a class. class Savings(Account): gets every method of Account, and can add or replace some. isinstance(s, Account) is true for a Savings.
The MRO (method resolution order) is the order Python searches classes for a method. Every class has one: AuditedSavings.__mro__. Python builds it so each class comes before its parents, and every class appears once:
| Position | Class | Its __init__ calls… |
|---|---|---|
| 1 | AuditedSavings | none of its own: Python uses the next one |
| 2 | Savings | super().__init__ → the next class in this MRO |
| 3 | Audited | super().__init__ → the next one |
| 4 | Account | sets owner, balance and log, and adds to OPENED |
| 5 | object | Python’s base for every class |
super() means “the next class in the MRO of this object”. In a plain Savings, that’s Account. In an AuditedSavings, it’s Audited. That’s how one chain of calls visits every class once.
Dunder methods (“double underscore”) let your objects work with Python’s own syntax: __repr__ for how it prints, __eq__ for ==, __len__ for len(), __add__ for +, __contains__ for in, and __iter__ for for.
Climb the ladder
Section titled “Climb the ladder”Every rung makes an account in O(1). What changes is how much code is repeated, and how many times the shared parent runs.
Copy the class
The idea: Write Savings as a full copy of Account, plus interest.
On Ana’s account: Savings("ana", 100) works, and add_interest() adds 2.
OPENED: list[str] = [] # every account the bank has opened
class Account: def __init__(self, owner: str, balance: int = 0) -> None: self.owner = owner self.balance = balance self.log = [f"open {owner}"] OPENED.append(owner)
def deposit(self, amount: int) -> None: self.balance += amount self.log.append(f"deposit {amount}")
class Savings: # a copy of Account, plus interest def __init__(self, owner: str, balance: int = 0, rate: float = 0.02) -> None: self.owner = owner self.balance = balance self.log = [f"open {owner}"] OPENED.append(owner) self.rate = rate
def deposit(self, amount: int) -> None: self.balance += amount self.log.append(f"deposit {amount}")
def add_interest(self) -> None: self.deposit(round(self.balance * self.rate))- Time
O(1)- Space
O(1)- Why
- Each account holds a few attributes.
Where it breaks: deposit lives in two places, so a fix in one misses the other. A Savings isn’t an Account: isinstance says no, so code that takes any account rejects it. And it prints as <Savings object at 0x…>.
Inherit, and call the parent by name
The idea: class Savings(Account) reuses the code, and Account.__init__(self, …) sets up the shared part.
The waste in Okay: the same methods written twice.
One parent works perfectly. Savings("bo", 50) runs Account.__init__ once, and isinstance(s, Account) is true.
Highlighted lines are new in this rung.
OPENED: list[str] = [] # every account the bank has opened
class Account: def __init__(self, owner: str, balance: int = 0) -> None: self.owner = owner self.balance = balance self.log = [f"open {owner}"] OPENED.append(owner)
def deposit(self, amount: int) -> None: self.balance += amount self.log.append(f"deposit {amount}")
class Savings(Account): def __init__(self, owner: str, balance: int = 0, rate: float = 0.02) -> None: Account.__init__(self, owner, balance) self.rate = rate
def add_interest(self) -> None: self.deposit(round(self.balance * self.rate))
class Audited(Account): def __init__(self, owner: str, balance: int = 0) -> None: Account.__init__(self, owner, balance) self.log.append("audit on")
class AuditedSavings(Savings, Audited): def __init__(self, owner: str, balance: int = 0, rate: float = 0.02) -> None: Savings.__init__(self, owner, balance, rate) Audited.__init__(self, owner, balance)- Time
O(1)- Space
O(1)- Why
- The same attributes; less code.
What changed from the previous rung
OPENED: list[str] = [] # every account the bank has opened
class Account: def __init__(self, owner: str, balance: int = 0) -> None: self.owner = owner self.balance = balance self.log = [f"open {owner}"] OPENED.append(owner)
def deposit(self, amount: int) -> None: self.balance += amount self.log.append(f"deposit {amount}")
class Savings: # a copy of Account, plus interestclass Savings(Account): def __init__(self, owner: str, balance: int = 0, rate: float = 0.02) -> None: self.owner = owner self.balance = balance self.log = [f"open {owner}"] OPENED.append(owner) Account.__init__(self, owner, balance) self.rate = rate
def deposit(self, amount: int) -> None: self.balance += amount self.log.append(f"deposit {amount}")
def add_interest(self) -> None: self.deposit(round(self.balance * self.rate))
class Audited(Account): def __init__(self, owner: str, balance: int = 0) -> None: Account.__init__(self, owner, balance) self.log.append("audit on")
class AuditedSavings(Savings, Audited): def __init__(self, owner: str, balance: int = 0, rate: float = 0.02) -> None: Savings.__init__(self, owner, balance, rate) Audited.__init__(self, owner, balance)Where it breaks: with two parents. AuditedSavings calls Savings.__init__ and Audited.__init__, and each calls Account.__init__. It runs twice, so OPENED holds Ana twice. Any setup in Account is done, then undone and done again.
super(), __repr__ and __eq__
The idea: Each __init__ calls super().__init__, so Python visits every class once in MRO order; add __repr__ and __eq__.
The waste in Better: each class decides who comes next, when Python already knows the right order.
On Ana’s account: Savings.__init__ calls super(), which is Audited for this object. Audited calls super(), which is Account. So Account runs once, then “audit on” is added, then the rate is set. AuditedSavings needs no __init__ at all.
__repr__ uses type(self).__name__, so every subclass prints its own name. __eq__ returns NotImplemented for things that aren’t accounts, so Python can try the other side.
Highlighted lines are new in this rung.
OPENED: list[str] = [] # every account the bank has opened
class Account: def __init__(self, owner: str, balance: int = 0) -> None: self.owner = owner self.balance = balance self.log = [f"open {owner}"] OPENED.append(owner)
def deposit(self, amount: int) -> None: self.balance += amount self.log.append(f"deposit {amount}")
def __repr__(self) -> str: return f"{type(self).__name__}({self.owner!r}, {self.balance})"
def __eq__(self, other: object) -> bool: if not isinstance(other, Account): return NotImplemented return (type(self), self.owner, self.balance) == (type(other), other.owner, other.balance)
class Savings(Account): def __init__(self, owner: str, balance: int = 0, rate: float = 0.02) -> None: super().__init__(owner, balance) self.rate = rate
def add_interest(self) -> None: self.deposit(round(self.balance * self.rate))
class Audited(Account): def __init__(self, owner: str, balance: int = 0) -> None: super().__init__(owner, balance) self.log.append("audit on")
class AuditedSavings(Savings, Audited): pass # Python runs Savings, then Audited, then Account: each __init__ once- Time
O(1)- Space
O(1)- Why
- Each __init__ runs once.
What changed from the previous rung
OPENED: list[str] = [] # every account the bank has opened
class Account: def __init__(self, owner: str, balance: int = 0) -> None: self.owner = owner self.balance = balance self.log = [f"open {owner}"] OPENED.append(owner)
def deposit(self, amount: int) -> None: self.balance += amount self.log.append(f"deposit {amount}")
def __repr__(self) -> str: return f"{type(self).__name__}({self.owner!r}, {self.balance})"
def __eq__(self, other: object) -> bool: if not isinstance(other, Account): return NotImplemented return (type(self), self.owner, self.balance) == (type(other), other.owner, other.balance)
class Savings(Account): def __init__(self, owner: str, balance: int = 0, rate: float = 0.02) -> None: Account.__init__(self, owner, balance) super().__init__(owner, balance) self.rate = rate
def add_interest(self) -> None: self.deposit(round(self.balance * self.rate))
class Audited(Account): def __init__(self, owner: str, balance: int = 0) -> None: Account.__init__(self, owner, balance) super().__init__(owner, balance) self.log.append("audit on")
class AuditedSavings(Savings, Audited): def __init__(self, owner: str, balance: int = 0, rate: float = 0.02) -> None: Savings.__init__(self, owner, balance, rate) Audited.__init__(self, owner, balance) pass # Python runs Savings, then Audited, then Account: each __init__ onceWhen Best isn’t best: deep class trees are hard to follow. If Audited only adds logging, a small helper object that the account holds is often clearer than another parent. Prefer composition, and keep inheritance shallow.
Write it yourself
Section titled “Write it yourself”The starter is the Better rung. Change it so that opening an AuditedSavings adds the owner to OPENED once. Then add __repr__ and __eq__ to Account. The tests make objects and then check them.
Open each account once
Ctrl + Enter runs- Not run yet:
AuditedSavings("ana", 100); OPENED→['ana'] - Not run yet:
AuditedSavings("ana", 100).log→['open ana', 'audit on'] - Not run yet:
AuditedSavings("ana", 100, rate=0.05).rate→0.05 - Not run yet:
repr(Account("ana", 100))→"Account('ana', 100)" - Not run yet:
Account("ana", 100) == Account("ana", 100)→True - Not run yet: Extra test
- Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Compare
Section titled “Compare”| Way | Repeated code | isinstance(s, Account) | Two parents | Use it when |
|---|---|---|---|---|
| Okay: copies | All of it | No | Not possible | Never |
Better: Parent.__init__ | None | Yes | Shared parent runs twice | One parent only, in old code |
Best: super() | None | Yes | Each class once | Always |
Common mistakes and edge cases
Section titled “Common mistakes and edge cases”__eq__without__hash__: defining__eq__makes objects unhashable, so they can’t go in a set or be dict keys. Add__hash__if they never change, or use a frozen dataclass.- Return
NotImplemented, don’t raise: from__eq__or__add__when the other value is a type you don’t handle. Python then tries the other side, or gives a clearTypeError. - Class attributes are shared:
class A: items = []is one list for every object. Set lists in__init__. - Different arguments in a chain: when parents take different arguments, pass the extras along with
**kwargsso eachsuper()call can take its own. __repr__or__str__:__repr__is for developers and should look like the code that makes the object.__str__is for users;print()uses it and falls back to__repr__.
Say it in the interview
Section titled “Say it in the interview”Questions to ask first
- Is this “is a” (inheritance) or “has a” (composition)?
- Will objects be compared, sorted, or used as dict keys?
A 60-second walkthrough
“The bug is calling
Account.__init__by name from both parents. With multiple inheritance, Python builds one method resolution order, here AuditedSavings, Savings, Audited, Account, object.super()doesn’t mean ‘my parent’: it means the next class in the MRO of this object. So if every class callssuper().__init__, each runs exactly once. I’d also add__repr__for debugging and__eq__that returnsNotImplementedfor other types.”
Likely follow-ups
- How is the MRO built? With the C3 linearization: each class comes before its parents, and the order of the parents in the
classline is kept. - Composition or inheritance? Inheritance for a true “is a” with shared behaviour. Composition, holding another object, for everything else; it is easier to change.
@classmethodor@staticmethod? The next lesson covers both.
Practice
Section titled “Practice”Three problems, from easy to hard. Each has hints, tests you can run here, and its own Okay, Better and Best answers. Try before you open them.
1. Money that adds up (easy)
Section titled “1. Money that adds up (easy)”Write a Money class that holds cents and a currency. Two Money objects with the same cents and currency are equal. + adds two amounts of the same currency, and raises ValueError("can't add USD to EUR") for different ones. Adding anything that isn’t Money is a TypeError.
Hint 1 of 3
Without __eq__, == checks whether two names point to the same object.
Hint 2 of 3
__add__(self, other) runs for self + other. Return a new Money.
Hint 3 of 3
Return NotImplemented for other types; Python then raises the TypeError for you.
Money that adds up
Ctrl + Enter runs- Not run yet:
Money(5, "EUR") == Money(5, "EUR")→True - Not run yet:
Money(1, "EUR") + Money(2, "EUR")→"Money(3, 'EUR')" - Not run yet:
Money(1, "EUR") + Money(2, "USD")→ raisesValueError - Not run yet: Extra test
- Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Show the Okay, Better and Best answers
Only __repr__
The idea: Store the two values and print them nicely.
Money(5, 'EUR') prints well.
class Money: def __init__(self, cents: int, currency: str) -> None: self.cents = cents self.currency = currency
def __repr__(self) -> str: return f"Money({self.cents}, {self.currency!r})"- Time
O(1)- Space
O(1)- Why
- Two attributes.
Where it breaks: Money(5, "EUR") == Money(5, "EUR") is False: without __eq__, == asks whether they are the same object. And + is a TypeError.
__eq__ and __add__
The idea: Compare cents and currency, and add the cents.
The waste in Okay: the object can’t take part in == or +.
Highlighted lines are new in this rung.
class Money: def __init__(self, cents: int, currency: str) -> None: self.cents = cents self.currency = currency
def __repr__(self) -> str: return f"Money({self.cents}, {self.currency!r})"
def __eq__(self, other: object) -> bool: if not isinstance(other, Money): return NotImplemented return (self.cents, self.currency) == (other.cents, other.currency)
def __add__(self, other: "Money") -> "Money": return Money(self.cents + other.cents, self.currency)- Time
O(1)- Space
O(1)- Why
- One sum.
What changed from the previous rung
class Money: def __init__(self, cents: int, currency: str) -> None: self.cents = cents self.currency = currency
def __repr__(self) -> str: return f"Money({self.cents}, {self.currency!r})"
def __eq__(self, other: object) -> bool: if not isinstance(other, Money): return NotImplemented return (self.cents, self.currency) == (other.cents, other.currency)
def __add__(self, other: "Money") -> "Money": return Money(self.cents + other.cents, self.currency)Where it breaks: 1 EUR + 2 USD gives 3 EUR, without a word.
Check the currency and the type
The idea: Raise ValueError for a different currency; return NotImplemented for other types.
The waste in Better: it trusts both amounts to be in the same currency.
Highlighted lines are new in this rung.
class Money: def __init__(self, cents: int, currency: str) -> None: self.cents = cents self.currency = currency
def __repr__(self) -> str: return f"Money({self.cents}, {self.currency!r})"
def __eq__(self, other: object) -> bool: if not isinstance(other, Money): return NotImplemented return (self.cents, self.currency) == (other.cents, other.currency)
def __add__(self, other: object) -> "Money": if not isinstance(other, Money): return NotImplemented if other.currency != self.currency: raise ValueError(f"can't add {other.currency} to {self.currency}") return Money(self.cents + other.cents, self.currency)- Time
O(1)- Space
O(1)- Why
- Two checks and one sum.
What changed from the previous rung
class Money: def __init__(self, cents: int, currency: str) -> None: self.cents = cents self.currency = currency
def __repr__(self) -> str: return f"Money({self.cents}, {self.currency!r})"
def __eq__(self, other: object) -> bool: if not isinstance(other, Money): return NotImplemented return (self.cents, self.currency) == (other.cents, other.currency)
def __add__(self, other: "Money") -> "Money": def __add__(self, other: object) -> "Money": if not isinstance(other, Money): return NotImplemented if other.currency != self.currency: raise ValueError(f"can't add {other.currency} to {self.currency}") return Money(self.cents + other.cents, self.currency)When Best isn’t best: for real money, a frozen dataclass gives __eq__ and __hash__ at no extra cost, and libraries like py-moneyed handle rounding and currencies.
2. Shapes that know their area (medium)
Section titled “2. Shapes that know their area (medium)”Write Circle(r) and Rect(w, h), each with an area() method, and total_area(shapes), rounded to 2 places. Adding a new shape must not mean changing total_area. A shape that forgets to define area() should fail with a clear NotImplementedError.
Hint 1 of 3
A chain of if kind == ... must change for every new shape.
Hint 2 of 3
If every shape has area(), total_area just calls it.
Hint 3 of 3
A base class Shape can raise NotImplementedError in its own area().
Shapes that know their area
Ctrl + Enter runs- Not run yet:
total_area([Circle(1), Rect(2, 3)])→9.14 - Not run yet:
total_area([])→0 - Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Show the Okay, Better and Best answers
Dicts and an if per kind
The idea: Keep shapes as dicts, and add up their areas with an if for each kind.
Circles and rectangles add up right.
import math
def total_area(shapes: list[dict]) -> float: total = 0.0 for s in shapes: if s["kind"] == "circle": total += math.pi * s["r"] ** 2 elif s["kind"] == "rect": total += s["w"] * s["h"] return round(total, 2)- Time
O(n)- Space
O(1)- Why
- One look at each shape.
Where it breaks: a square is silently skipped. Every new kind means editing total_area.
A class per shape
The idea: Each class has its own area(), and total_area calls it.
The waste in Okay: total_area knows every kind of shape.
Now total_area never changes. Each shape answers for itself; this is called polymorphism.
Highlighted lines are new in this rung.
import math
class Circle: def __init__(self, r: float) -> None: self.r = r
def area(self) -> float: return math.pi * self.r**2
class Rect: def __init__(self, w: float, h: float) -> None: self.w = w self.h = h
def area(self) -> float: return self.w * self.h
def total_area(shapes: list) -> float: return round(sum(s.area() for s in shapes), 2)- Time
O(n)- Space
O(1)- Why
- One call per shape.
What changed from the previous rung
import math
def total_area(shapes: list[dict]) -> float: total = 0.0 for s in shapes: if s["kind"] == "circle": total += math.pi * s["r"] ** 2 elif s["kind"] == "rect": total += s["w"] * s["h"] return round(total, 2)class Circle: def __init__(self, r: float) -> None: self.r = r
def area(self) -> float: return math.pi * self.r**2
class Rect: def __init__(self, w: float, h: float) -> None: self.w = w self.h = h
def area(self) -> float: return self.w * self.h
def total_area(shapes: list) -> float: return round(sum(s.area() for s in shapes), 2)Where it breaks: nothing says a shape must have area(). One that forgets fails deep inside sum() with AttributeError.
A base class
The idea: Shape.area() raises NotImplementedError, and a Square can reuse Rect with super().
The waste in Better: the shared rule, “every shape has an area”, isn’t written anywhere.
Highlighted lines are new in this rung.
import math
class Shape: def area(self) -> float: raise NotImplementedError(f"{type(self).__name__} must define area()")
class Circle(Shape): def __init__(self, r: float) -> None: self.r = r
def area(self) -> float: return math.pi * self.r**2
class Rect(Shape): def __init__(self, w: float, h: float) -> None: self.w = w self.h = h
def area(self) -> float: return self.w * self.h
class Square(Rect): def __init__(self, side: float) -> None: super().__init__(side, side)
def total_area(shapes: list[Shape]) -> float: return round(sum(s.area() for s in shapes), 2)- Time
O(n)- Space
O(1)- Why
- One call per shape.
What changed from the previous rung
import math
class Circle:class Shape: def area(self) -> float: raise NotImplementedError(f"{type(self).__name__} must define area()")
class Circle(Shape): def __init__(self, r: float) -> None: self.r = r
def area(self) -> float: return math.pi * self.r**2
class Rect:class Rect(Shape): def __init__(self, w: float, h: float) -> None: self.w = w self.h = h
def area(self) -> float: return self.w * self.h
def total_area(shapes: list) -> float:class Square(Rect): def __init__(self, side: float) -> None: super().__init__(side, side)
def total_area(shapes: list[Shape]) -> float: return round(sum(s.area() for s in shapes), 2)When Best isn’t best: abc.ABC with @abstractmethod stops you making a shape without area() at all, which is stricter. The ABCs and Protocols lesson compares the two.
3. A stack that works with len(), in and for (hard)
Section titled “3. A stack that works with len(), in and for (hard)”Write a Stack class with push(item). Make Python’s own tools work on it: len(s), x in s, for x in s (top of the stack first), list(s), and if s: for an empty stack. It prints as Stack([1, 2, 3]).
Hint 1 of 3
len() calls __len__, and in calls __contains__.
Hint 2 of 3
for and list() call __iter__, which returns an iterator.
Hint 3 of 3
An object with __len__ counts as false when it returns 0.
A stack that works with len(), in and for
Ctrl + Enter runs- Not run yet:
s.push(1); s.push(2); len(s)→2 - Not run yet:
s.push(1); [1 in s, 3 in s]→[True, False] - Not run yet:
push 1, 2, 3; list(s)→[3, 2, 1] - Not run yet: Extra test
- Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Show the Okay, Better and Best answers
Methods with our own names
The idea: Add size() and has() methods.
s.size() and s.has(1) work.
class Stack: def __init__(self) -> None: self.items: list = []
def push(self, item: object) -> None: self.items.append(item)
def size(self) -> int: return len(self.items)
def has(self, item: object) -> bool: return item in self.items- Time
O(n) for has- Space
O(n)- Why
- has() checks the items one by one.
Where it breaks: len(s) is a TypeError. Every user must learn your method names instead of Python’s.
__len__ and __contains__
The idea: Name the methods so len() and in call them.
The waste in Okay: it invents names for things Python already has names for.
len(s) and 1 in s work, and so does if s:, through __len__.
Highlighted lines are new in this rung.
class Stack: def __init__(self) -> None: self.items: list = []
def push(self, item: object) -> None: self.items.append(item)
def __len__(self) -> int: return len(self.items)
def __contains__(self, item: object) -> bool: return item in self.items- Time
O(n) for in- Space
O(n)- Why
- in checks the items one by one.
What changed from the previous rung
class Stack: def __init__(self) -> None: self.items: list = []
def push(self, item: object) -> None: self.items.append(item)
def size(self) -> int: def __len__(self) -> int: return len(self.items)
def has(self, item: object) -> bool: def __contains__(self, item: object) -> bool: return item in self.itemsWhere it breaks: for x in s and list(s) raise TypeError: 'Stack' object is not iterable.
__iter__ and __repr__ too
The idea: __iter__ returns reversed(self.items), so for loops see the top first.
The waste in Better: a container you can’t loop over.
Highlighted lines are new in this rung.
from collections.abc import Iterator
class Stack: def __init__(self) -> None: self.items: list = []
def push(self, item: object) -> None: self.items.append(item)
def __len__(self) -> int: return len(self.items)
def __contains__(self, item: object) -> bool: return item in self.items
def __iter__(self) -> Iterator: return reversed(self.items) # top of the stack first
def __repr__(self) -> str: return f"Stack({self.items!r})"- Time
O(n) to loop- Space
O(n)- Why
- reversed() walks the list without copying it.
What changed from the previous rung
from collections.abc import Iterator
class Stack: def __init__(self) -> None: self.items: list = []
def push(self, item: object) -> None: self.items.append(item)
def __len__(self) -> int: return len(self.items)
def __contains__(self, item: object) -> bool: return item in self.items
def __iter__(self) -> Iterator: return reversed(self.items) # top of the stack first
def __repr__(self) -> str: return f"Stack({self.items!r})"When Best isn’t best: a plain list already is a stack, with append() and pop(). Write a class only when you need rules a list can’t keep.
Check yourself
Section titled “Check yourself”A few quick questions. Ones you miss are remembered, so you can find your weak spots later.
Flashcards
Section titled “Flashcards”Add these to your review deck. Each card comes back a little later every time you get it right.
Cheat sheet
Section titled “Cheat sheet”class B(A):inherits;super().method()calls the next class in the MRO.Cls.__mro__shows the order Python searches. Withsuper(), every class in it runs once.__repr__for printing,__eq__for==(returnNotImplementedfor other types).__len__,__contains__,__iter__,__add__makelen(),in,forand+work on your objects.- Defining
__eq__removes__hash__; prefer composition over deep inheritance.
Go deeper
Section titled “Go deeper”- Classes, in the Python tutorial, including multiple inheritance.
super()and the data model, for every dunder method.- The Python 2.3 method resolution order, on how the MRO is built.
- Next:
@property, class methods and static methods.
Can you do these now?
If you’re not sure of any of these, scroll back to that part before moving on.
Status: Not started