Exercises — Instance methods, class methods (`@classmethod`), static methods (`@staticmethod`)
Level 1 — Recognition
(Can you name the kind of method and its auto-argument, without running anything?)
Exercise 1.1
Look at this class. For each of the three methods, state (a) its kind, and (b) what its first parameter receives when called.
class Box:
count = 0
def __init__(self, w): self.w = w
def volume(self): return self.w ** 3
@classmethod
def how_many(cls): return cls.count
@staticmethod
def label(): return "BOX"Recall Solution 1.1
| method | kind | first parameter receives |
|---|---|---|
volume |
instance method | ==self== — the specific Box object it was called on |
how_many |
class method (@classmethod) |
==cls== — the class Box itself |
label |
static method (@staticmethod) |
nothing — no auto first argument |
Why: a plain def inside a class always injects the object as the first positional slot; @classmethod swaps that injection to the class; @staticmethod turns injection off entirely.
Exercise 1.2
b = Box(2). Rewrite each of these calls as the explicit function call Python actually performs:
b.volume(), Box.how_many(), Box.label().
Recall Solution 1.2
b.volume()Box.volume(b)— the objectbslips into theselfslot.Box.how_many()how_many(Box)— the class slips into theclsslot.Box.label()label()— no auto argument is added at all.
Level 2 — Application
(Predict the printed output.)
Exercise 2.1
class Circle:
PI = 3.14159
def __init__(self, r): self.r = r
def area(self): return Circle.PI * self.r ** 2
c = Circle(2)
print(round(c.area(), 3))What prints?
Recall Solution 2.1
area is an instance method, so c.area() becomes Circle.area(c); here self.r = 2.
Rounded to 3 decimals: 12.566.
Exercise 2.2
class Animal:
species = "generic"
@classmethod
def describe(cls): return f"I am a {cls.species}"
class Dog(Animal):
species = "dog"
print(Animal.describe())
print(Dog.describe())What are the two printed lines?
Recall Solution 2.2
describe is a class method, so its cls is whatever class the call came through.
Animal.describe()→cls = Animal→ readsAnimal.species = "generic"→I am a generic.Dog.describe()→cls = Dog→ readsDog.species = "dog"→I am a dog.
This is the whole point of cls: it follows the call, not the definition site. Related idea: Inheritance and method resolution order (MRO).
Exercise 2.3
class Date:
def __init__(self, y, m, d): self.y, self.m, self.d = y, m, d
@classmethod
def from_string(cls, s):
y, m, d = map(int, s.split("-"))
return cls(y, m, d)
d = Date.from_string("2024-05-01")
print(d.y, d.m, d.d)What prints?
Recall Solution 2.3
"2024-05-01".split("-") → ["2024","05","01"], and map(int, ...) converts to 2024, 5, 1.
cls(2024, 5, 1) builds a Date, and we print its attributes: 2024 5 1.
This is the alternate-constructor pattern — one __init__, many named ways to build.
Level 3 — Analysis
(Now explain / spot the bug, not just predict.)
Exercise 3.1
This code raises an error. State the exact error message type and explain the root cause.
class Circle:
def area(): # note: no self
return self.r ** 2
c = Circle()
c.area()Recall Solution 3.1
Error: TypeError: area() takes 0 positional arguments but 1 was given.
Root cause: c.area() is compiled to Circle.area(c) — Python always passes the object into the first positional slot. But def area(): declares zero parameters, so there's no slot to receive c. (Even if there were, self.r would be undefined because self was never a name here.)
Fix: def area(self): return self.r ** 2.
Exercise 3.2
Predict the two printed lines, then explain why the second line is what it is.
class Counter:
total = 0
def bump(self):
self.total = self.total + 1 # note: self.total
c = Counter()
c.bump(); c.bump()
print(Counter.total)
print(c.total)Recall Solution 3.2
Prints 0 then 2.
Why: reading self.total finds nothing on the instance, so it falls back to the class (0). But assigning self.total = ... always creates an instance attribute — it never touches Counter.total. So after two bumps the object has its own total = 2, while the class attribute stays 0.
Fix if you wanted a shared counter: assign Counter.total = Counter.total + 1 (or use cls.total inside a classmethod). See Instance vs Class attributes.
Exercise 3.3
Point stores 2D coordinates. Complete distance_from_origin as an instance method returning the Euclidean distance. Then evaluate it for Point(3, 4).
class Point:
def __init__(self, x, y): self.x, self.y = x, y
def distance_from_origin(self):
... # fill in(See the figure — the distance is the straight arrow from the origin to the point.)

Recall Solution 3.3
def distance_from_origin(self):
return (self.x ** 2 + self.y ** 2) ** 0.5This must be an instance method: it depends on this specific point's self.x, self.y.
For Point(3, 4):
Result: 5.0.
Level 4 — Synthesis
(Design decisions: choose the right method kind and justify it.)
Exercise 4.1
Given the parent's Circle, why does the factory below break for subclasses? Write the corrected version and predict type(Ball.unit()).
class Circle:
def __init__(self, r): self.r = r
@staticmethod
def unit():
return Circle(1) # <-- suspicious
class Ball(Circle): passRecall Solution 4.1
Bug: @staticmethod receives no class info, so it hard-codes Circle(1). Calling Ball.unit() still returns a plain Circle, ignoring the subclass.
Fix — use @classmethod + cls:
@classmethod
def unit(cls):
return cls(1)Now Ball.unit() runs with cls = Ball, so it builds a Ball. Prediction: type(Ball.unit()) is Ball (i.e. <class '...Ball'>). This is the reason factories use @classmethod. Compare Factory design pattern.
Exercise 4.2
You have three related functions. For each, decide the correct method kind and give one sentence of justification.
perimeter()— needs the object's radius.from_diameter(d)— builds a circle from a diameter, must respect subclasses.is_valid_radius(r)— checksr > 0, uses no object and no class data.
Recall Solution 4.2
perimeter→ instance method (def perimeter(self):): it readsself.r, which is per-object.from_diameter→ class method (@classmethod def from_diameter(cls, d): return cls(d / 2)): a factory that must build the calling class, so it needscls.is_valid_radius→ static method (@staticmethod def is_valid_radius(r): return r > 0): pure logic tied to the topic but needing neitherselfnorcls.
Level 5 — Mastery
(The mechanism itself — descriptors — and multi-layer inheritance.)
Exercise 5.1
Using only the descriptor identity from the parent
explain in words why Circle.area(c) and c.area() give the same result, and what object c.area (without parentheses) actually is.
Recall Solution 5.1
A function is a descriptor — it implements __get__. When you write c.area, Python calls Circle.__dict__['area'].__get__(c, Circle), which returns a bound method: a small object that remembers c and, when called, forwards to area(c, ...).
- So
c.area(no parens) is a bound method object, not the raw function. - Calling
c.area()therefore runsarea(c)=Circle.area(c). They are the same call written two ways.
Deeper reading: Descriptors and the __get__ protocol and Decorators (which is how @classmethod/@staticmethod swap the descriptor).
Exercise 5.2
Predict the exact printed output. Explain each line using MRO + cls binding.
class A:
x = 10
@classmethod
def f(cls): return cls.x
class B(A):
x = 20
class C(B):
pass
print(A.f(), B.f(), C.f())Recall Solution 5.2
Prints 10 20 20.
A.f()→cls = A→A.x = 10.B.f()→cls = B→B.x = 20.C.f()→cls = C;Chas nox, so the method resolution order searchesC → B → Aand findsx = 20onBfirst. Sincecls = C,cls.xlooks it up alongC's MRO →20.fis defined only once onA, yet each call reads the rightxbecauseclsis the calling class. See Inheritance and method resolution order (MRO).
Exercise 5.3
A Temperature stores Celsius. Add a class method from_fahrenheit(cls, f) using . Evaluate Temperature.from_fahrenheit(212).c and Temperature.from_fahrenheit(32).c.

Recall Solution 5.3
class Temperature:
def __init__(self, c): self.c = c
@classmethod
def from_fahrenheit(cls, f):
return cls((f - 32) * 5 / 9)from_fahrenheit(212): →100.0.from_fahrenheit(32): →0.0.
Class method (not static) so a subclass class Kelvinish(Temperature) would build its own type.
Return to the parent topic.