2.1.4 · D4OOP Fundamentals

Exercises — Instance methods, class methods (`@classmethod`), static methods (`@staticmethod`)

2,045 words9 min readBack to topic

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 object b slips into the self slot.
  • Box.how_many() how_many(Box) — the class slips into the cls slot.
  • 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 → reads Animal.species = "generic"I am a generic.
  • Dog.describe()cls = Dog → reads Dog.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.)

Figure — Instance methods, class methods (`@classmethod`), static methods (`@staticmethod`)
Recall Solution 3.3
def distance_from_origin(self):
    return (self.x ** 2 + self.y ** 2) ** 0.5

This 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): pass
Recall 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.

  1. perimeter() — needs the object's radius.
  2. from_diameter(d) — builds a circle from a diameter, must respect subclasses.
  3. is_valid_radius(r) — checks r > 0, uses no object and no class data.
Recall Solution 4.2
  1. perimeterinstance method (def perimeter(self):): it reads self.r, which is per-object.
  2. from_diameterclass method (@classmethod def from_diameter(cls, d): return cls(d / 2)): a factory that must build the calling class, so it needs cls.
  3. is_valid_radiusstatic method (@staticmethod def is_valid_radius(r): return r > 0): pure logic tied to the topic but needing neither self nor cls.

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 runs area(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 = AA.x = 10.
  • B.f()cls = BB.x = 20.
  • C.f()cls = C; C has no x, so the method resolution order searches C → B → A and finds x = 20 on B first. Since cls = C, cls.x looks it up along C's MRO → 20. f is defined only once on A, yet each call reads the right x because cls is 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.

Figure — Instance methods, class methods (`@classmethod`), static methods (`@staticmethod`)
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.