Worked examples — Instance methods, class methods (`@classmethod`), static methods (`@staticmethod`)
We only assume the vocabulary already built in OOP Fundamentals - Classes and Objects and Instance vs Class attributes. If a word is new, we rebuild it here from zero.
The scenario matrix
Every question this topic can ask you falls into one of these cells. Think of the columns as which method kind and the rows as what twist the question adds.
| # | Twist / edge case | Instance method (self) |
Class method (cls) |
Static method (none) |
|---|---|---|---|---|
| A | Plain call, no inheritance | Ex 1 | Ex 1 | Ex 1 |
| B | Called through the class vs through an instance | Ex 2 | Ex 2 | — |
| C | Subclass overrides an attribute | — | Ex 3 | — |
| D | Factory / alternate constructor with subclass | — | Ex 4 | Ex 4 (the wrong way) |
| E | Missing / wrong first argument (degenerate call) | Ex 5 | Ex 5 | Ex 5 |
| F | Assigning vs reading shared state (shadowing) | Ex 6 | Ex 6 | — |
| G | Real-world word problem | Ex 7 | Ex 7 | Ex 7 |
| H | Exam twist: static calling classmethod, MRO surprise | — | Ex 8 | Ex 8 |

Ex 1 — Cell A: all three, plain call, one class
Steps.
c.area(). Why this step?areahasselfas its first parameter, so Python secretly rewritesc.area()intoCircle.area(c)— the cookiecslides intoself. Then , which rounds to12.566.Circle.unit(). Why this step?unitis a@classmethod, soclsis filled withCircle(the cutter itself, no cookie needed). It runscls(1) = Circle(1), a new circle with.r == 1. So.rprints1.Circle.is_valid(-1). Why this step?is_validis@staticmethod— nothing is auto-injected. The-1you typed lands directly inr. Then-1 > 0isFalse.
Recall
Line 1 output ::: 12.566
Line 3 output ::: False
Ex 2 — Cell B: instance method called through the class
Steps.
c.area(). Why this step? Recall the descriptor__get__we defined above — the hidden mechanism that fills the first slot. When you writec.area, that mechanism bindscintoselfautomatically, producingCircle.area(c).Circle.area(c). Why this step? Here you supply the object manually. There is no auto-injection when you go through the class name, so you must passcyourself.- Both compute .
Recall
Are the two outputs equal? ::: Yes, both are 12.566.
Ex 3 — Cell C: subclass overrides an attribute (why cls, not the class name)
Steps.
Animal.describe(). Why this step? Call comes throughAnimal, socls = Animal, andcls.speciesreads"generic".Dog.describe(). Why this step?Dogdid not definedescribe, so Python walks the MRO (the child-up-to-parent search list we defined above) and findsAnimal.describe. But the call came throughDog, socls = Dog. Nowcls.speciesreadsDog.species = "dog". See Inheritance and method resolution order (MRO).- Why not hard-code
Animal.species? Then step 2 would ignore the override and wrongly say"generic".clsis what makes the method inheritance-aware.
Recall
Animal.describe() ::: "I am a generic"
Dog.describe() ::: "I am a dog"
Ex 4 — Cell D: factory the right way vs the wrong way
Steps.
Star.triangle_good(). Why this step? It is a@classmethod; calling throughStarsetscls = Star, socls(3) = Star(3). The type isStar.Star.triangle_bad(). Why this step? It is@staticmethod— it cannot see the calling class and literally namesShape. So it builds aShapeeven though you called throughStar. The type isShape.- Why this matters. A subclass wanting
Star-flavoured objects silently gets baseShapeobjects — a bug that hides until much later. This is the killer reason factories use@classmethod. See Factory design pattern.
Recall
type(Star.triangle_good()).__name__ ::: "Star"
type(Star.triangle_bad()).__name__ ::: "Shape"
Ex 5 — Cell E: the degenerate calls (missing / extra first argument)
Steps.
(i) a.inst(). Why? Auto-fillsself = a. ✅ runs.(ii) A.inst(). Why? Through the class name there is no auto-injection, and no argument is supplied, soselfhas nothing. →TypeError: inst() missing 1 required positional argument: 'self'. ❌.(iii) a.cm(). Why?@classmethodinjectscls = Ano matter whether you call throughaorA. ✅ runs.(iv) a.st(). Why?@staticmethodinjects nothing and needs nothing. ✅ runs.(v) A.st(99). Why? Static wants zero parameters, but you handed it99. →TypeError: st() takes 0 positional arguments but 1 was given. ❌.
Recall
Which two lines raise TypeError? ::: (ii) A.inst() and (v) A.st(99).
Ex 6 — Cell F: reading vs assigning shared state (shadowing)
Steps.
a.bump_wrong()twice. Why this step?self.total += 1expands toself.total = self.total + 1. The right side readstotal(falls back to the class →0, then1), but the assignment always creates an instance attribute ona. So the class'stotalnever moves; onlya.totalgrows to2.print(Counter.total, a.total)→0 2. Why? Class stayed0;anow shadows it with2.Counter.bump_right(). Why this step?cls.total += 1targets the class attribute, soCounter.totalbecomes1.print(Counter.total, b.total)→1 1. Why?bnever got its own attribute, sob.totalfalls back to the class value1.
Recall
Line 1 output ::: 0 2
Line 2 output ::: 1 1
Ex 7 — Cell G: real-world word problem (parsing dates & bank fees)
Steps.
from_dollars_cents(100, 50). Why classmethod? It builds an account a second way;cls(...)keeps it subclass-friendly. Balance dollars.acc.yearly_interest(). Why instance? Interest depends on this account's balance: dollars/year.Account.is_legal(-3). Why static? The legality rule needs neither an account nor the class — just the number. isFalse.
Recall
acc.balance ::: 100.5
acc.yearly_interest() ::: 5.025
Account.is_legal(-3) ::: False
Ex 8 — Cell H: exam twist (a static method that reaches back into the class, and an MRO surprise)
Steps.
Base.label()→cls = Base, socls.tag = "B". Output"B".Sub.label()→cls = Sub(inheritance-aware), socls.tag = "S". Output"S".Sub.shout(). Why the trap?shoutis a@staticmethod: it has nocls. Inside it we hard-codedBase.label(), which forcescls = Base→"B", then appends"!". Output"B!"— not"S!", even though you called throughSub. A static method can't respect the calling class; that is exactly what makes it wrong for anything inheritance-sensitive.
Recall
Base.label() ::: "B"
Sub.label() ::: "S"
Sub.shout() ::: "B!"
The whole matrix in one diagram
The flowchart below is the decision you make every time you write a method: start at the top with the question "what does this method actually need?", and each branch drops you onto the right kind. The two leaf notes (factory, loses subclass awareness) are the payoff cells D and H we just worked — they show why the branch choice matters, not just what it is called.
Read it as: need this cookie's data → self (Ex 1, 2, 6, 7). Need the cutter, maybe to stamp new cookies → cls (Ex 3, 4, 7). Need neither → static (Ex 1, 5, 8) — but notice the dashed danger branch: a static method that names a class by hand cannot follow a subclass, which is precisely the Ex 8 trap.
Flashcards
Rewrite c.area() as an explicit function call.
Circle.area(c) — the object becomes the first positional argument.Why does A.inst() raise TypeError while a.inst() runs?
self is left empty.Does self.total += 1 change the class attribute total?
Why does a @staticmethod factory break subclasses?
What does Sub.shout() return if shout is static and calls Base.label()?
"B!" — static has no cls, so it uses Base, ignoring Sub.