Learning Goals
3 minBy the end of this lesson you can:
- Use
@staticmethodfor methods that don't needselforcls— utility functions that belong inside a class for organisational reasons. - Use
@classmethodfor methods that take the class itself — typically alternate constructors. - Tell them apart from regular instance methods at a glance.
- Decide which of the three method types fits a given job.
Warm-Up · Three Method Flavours
5 minFlavour First parameter Use when instance method self needs to read/write instance data classmethod cls needs the class itself (e.g. alt constructor) staticmethod nothing a utility related to the class but using neither
You've been using instance methods for 18 lessons. Today we meet the other two.
All three are defined inside a class, all three are decorated differently, and all three are called differently. Pick the right flavour for the job.
New Concept · The Three Flavours
14 min@staticmethod — no self, no cls
A static method is a regular function that happens to live in a class's namespace. It takes no automatic first argument.
class Math: @staticmethod def add(a, b): return a + b print(Math.add(3, 4)) # → 7 -- called on the CLASS print(Math().add(3, 4)) # → 7 -- also works on an instance, but unusual
Why bother putting it in a class then? Organisation. If add, subtract, multiply all belong with Math conceptually, they live there. Static methods don't use self; they don't need a class either — but grouping them inside one keeps related utilities together.
When to reach for @staticmethod
- The method's logic doesn't reference any instance or class state.
- It still belongs conceptually to the class (e.g. a helper used internally).
- Otherwise, just write a module-level function instead.
@classmethod — first parameter is cls
A class method takes the class itself as the first argument, conventionally named cls. Mostly used for alternate constructors.
class Hero: def __init__(self, name, hp, attack_power): self.name = name self.hp = hp self.attack_power = attack_power @classmethod def warrior(cls, name): """Alternate constructor — pre-set stats for a warrior.""" return cls(name, hp=150, attack_power=15) @classmethod def mage(cls, name): return cls(name, hp=80, attack_power=22) @classmethod def from_csv(cls, line): """Build from a 'name,hp,atk' string.""" name, hp, atk = line.split(",") return cls(name.strip(), int(hp), int(atk)) # Three ways to make a Hero h1 = Hero("Aisyah", 100, 12) # normal constructor h2 = Hero.warrior("Wei Jie") # alt — pre-set warrior stats h3 = Hero.from_csv("Priya, 80, 18") # alt — parse a CSV line
Notice all three return cls(...) — that's why cls matters. If a child class inherits these methods, cls is the child, and the alt constructor builds the right type automatically.
The inheritance benefit of cls
class PaladinHero(Hero): pass h = PaladinHero.warrior("Iman") # uses Hero.warrior, but cls = PaladinHero print(type(h)) # → PaladinHero -- not Hero!
If warrior had hard-coded return Hero(...), it would build a Hero even when called on PaladinHero. cls(...) uses whatever class the call started from. That's the whole reason classmethods exist.
Reading data on the class itself
Sometimes you want to read class-level constants:
class Pizza: BASE_PRICE = 8.0 # class attribute @classmethod def menu_price(cls, toppings): return cls.BASE_PRICE + len(toppings) * 1.50 print(Pizza.menu_price(["cheese", "mushroom"])) # 11.0
The classmethod can read cls.BASE_PRICE — and if a child class overrides it (e.g. LuxuryPizza.BASE_PRICE = 20), the same code uses the new value.
How to choose
Does the method use self? → instance method (def m(self, ...):) Does it need to know cls? → @classmethod (def m(cls, ...):) Neither, but conceptually groups with the class? → @staticmethod (def m(...):) Neither, and standalone? → module function (just def m(): outside any class)
Worked Example · Date with Alt Constructors
12 minBuild a tiny Date class that demonstrates all three method flavours. Save as date_ish.py:
# date_ish.py — Date with @classmethod and @staticmethod class Date: def __init__(self, year, month, day): if not Date.is_valid(year, month, day): raise ValueError(f"Invalid date: {year}-{month}-{day}") self.year = year self.month = month self.day = day # --- alternate constructors (@classmethod) --- @classmethod def from_iso(cls, text): """Build from 'YYYY-MM-DD' string.""" year, month, day = text.split("-") return cls(int(year), int(month), int(day)) @classmethod def from_uk(cls, text): """Build from 'DD/MM/YYYY' string.""" day, month, year = text.split("/") return cls(int(year), int(month), int(day)) @classmethod def today(cls): """Build today's date.""" from datetime import date d = date.today() return cls(d.year, d.month, d.day) # --- pure utility (@staticmethod) --- @staticmethod def is_valid(year, month, day): if not (1 <= month <= 12): return False days_in_month = [31, 29 if Date.is_leap(year) else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] return 1 <= day <= days_in_month[month - 1] @staticmethod def is_leap(year): return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0) # --- instance method (regular self) --- def iso(self): return f"{self.year:04d}-{self.month:02d}-{self.day:02d}" def __repr__(self): return f"Date({self.year}, {self.month}, {self.day})" # Build dates four different ways d1 = Date(2026, 5, 27) d2 = Date.from_iso("2026-12-31") d3 = Date.from_uk("27/05/2026") d4 = Date.today() for d in (d1, d2, d3, d4): print(d.iso(), d) # Static helpers — callable without an instance print(Date.is_leap(2024)) # True print(Date.is_leap(2025)) # False print(Date.is_valid(2026, 2, 30)) # False — Feb 30 doesn't exist
Read the diff
Three classmethod constructors (from_iso, from_uk, today) — each parses or sources different data, all funnel through cls(year, month, day). Two staticmethods (is_valid, is_leap) — pure calendar logic that doesn't need a Date instance. One instance method (iso) — uses self to read this date's fields. The class organises all of them in one place.
Try It Yourself
13 minAdd a @staticmethod to Point that computes the distance between two points.
Hint
import math class Point: def __init__(self, x, y): self.x = x; self.y = y @staticmethod def distance(p1, p2): return math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) a = Point(0, 0); b = Point(3, 4) print(Point.distance(a, b)) # 5.0
No self or cls needed — just two arguments. Conceptually distance "belongs" with Point, so it lives inside.
Add a @classmethod tank to Hero — pre-set HP 200, attack 8.
Hint
class Hero: def __init__(self, name, hp, attack): self.name = name; self.hp = hp; self.attack = attack @classmethod def tank(cls, name): return cls(name, hp=200, attack=8) iman = Hero.tank("Iman") print(iman.hp, iman.attack) # 200 8
Same shape as Hero.warrior from the concept section. Alt constructors are great for "named presets".
Make a User class that tracks how many users have been created. Use a class attribute _count and a @classmethod how_many that returns it.
Hint
class User: _count = 0 # class attribute — shared across all instances def __init__(self, name): self.name = name User._count += 1 # increment the class-level counter @classmethod def how_many(cls): return cls._count a = User("Aisyah") b = User("Wei Jie") c = User("Priya") print(User.how_many()) # 3
Class attributes live on the class, not on each instance. cls._count reads the same number regardless of which user we're looking at.
Mini-Challenge · Money With Conversions
8 minTake the Money class from PY-L3-16. Add three classmethods and one staticmethod:
@classmethod from_string(text)— parse"RM 12.50"into a Money.@classmethod zero(currency)— return a Money with amount 0 in given currency.@classmethod sum_in(currency, monies)— sum a list, requiring all to be incurrency.@staticmethod is_valid_currency(c)— return True if c is in{"RM", "USD", "EUR", "GBP"}.
Show one possible solution
# money_v2.py — Money with all method flavours class Money: VALID = {"RM", "USD", "EUR", "GBP"} def __init__(self, amount, currency="RM"): if not Money.is_valid_currency(currency): raise ValueError(f"Bad currency: {currency}") self.amount = round(float(amount), 2) self.currency = currency @classmethod def from_string(cls, text): currency, amount = text.split() return cls(amount, currency) @classmethod def zero(cls, currency): return cls(0, currency) @classmethod def sum_in(cls, currency, monies): for m in monies: if m.currency != currency: raise ValueError(f"Mixed currencies: expected {currency}, got {m.currency}") return cls(sum(m.amount for m in monies), currency) @staticmethod def is_valid_currency(c): return c in Money.VALID def __repr__(self): return f"Money({self.amount}, {self.currency!r})" print(Money.from_string("RM 12.50")) # Money(12.5, 'RM') print(Money.zero("USD")) # Money(0.0, 'USD') basket = [Money(5), Money(3, "RM"), Money(7.50)] print(Money.sum_in("RM", basket)) # Money(15.5, 'RM') print(Money.is_valid_currency("RM")) # True print(Money.is_valid_currency("YEN")) # False
Non-negotiables: three classmethods that each return a new instance, and one staticmethod that's pure utility. Mix-and-match flavours in one class — common pattern.
Recap
3 minThree method flavours. Instance methods (the default) take self. Class methods (@classmethod) take cls and are perfect for alternate constructors. Static methods (@staticmethod) take neither and group utility logic with the class. Classmethods using cls stay correct under inheritance — they build the right type automatically. Reach for the simplest flavour the job allows.
Vocabulary Card
- @classmethod
- First parameter is
cls(the class itself). Used for alternate constructors and class-level state. - @staticmethod
- No automatic first parameter. Pure utility, grouped with the class for organisation.
- alternate constructor
- A classmethod that builds and returns a new instance, often from a parsed string or external format.
- class attribute
- Data attached to the class itself, shared by every instance. Defined directly inside the class body.
Homework
4 minBuild color.py. A Color class storing r, g, b (0-255 each). Add:
__init__(r, g, b)— validates each is in range.@classmethod from_hex(s)— parse"#FF6347"into a Color.@classmethod red()/@classmethod green()/@classmethod blue()— return red/green/blue presets.@staticmethod is_valid_channel(n)— True if 0 ≤ n ≤ 255.- Instance method
to_hex()— return"#RRGGBB".
Sample · color.py
class Color: def __init__(self, r, g, b): for ch in (r, g, b): if not Color.is_valid_channel(ch): raise ValueError(f"Channel out of range: {ch}") self.r = r; self.g = g; self.b = b @classmethod def from_hex(cls, s): s = s.lstrip("#") return cls(int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16)) @classmethod def red(cls): return cls(255, 0, 0) @classmethod def green(cls): return cls(0, 255, 0) @classmethod def blue(cls): return cls(0, 0, 255) @staticmethod def is_valid_channel(n): return isinstance(n, int) and 0 <= n <= 255 def to_hex(self): return f"#{self.r:02X}{self.g:02X}{self.b:02X}" def __repr__(self): return f"Color(r={self.r}, g={self.g}, b={self.b})" c1 = Color(255, 99, 71) c2 = Color.from_hex("#FF6347") c3 = Color.red() print(c1.to_hex()) # #FF6347 print(c1 == c2) # need __eq__ for True — try adding it print(c3) # Color(r=255, g=0, b=0)
Non-negotiables: three classmethods (one parser + three preset constructors), one staticmethod for validation. The named constructors (red(), green()) are clean alternatives to magic constants — and inheritance-safe.