Method Types
Python classes have three kinds of methods: instance methods, class methods (@classmethod), and static methods (@staticmethod). Understanding the purpose and the right time to use each leads to cleaner, more maintainable code.
Instance Methods
Instance methods are the most common type. They receive self as the first parameter, giving access to the instance's attributes for reading or modification.
class Circle:
PI = 3.14159265358979
def __init__(self, radius: float):
self.radius = radius
# Instance method: accesses instance attributes via self
def area(self) -> float:
return self.PI * self.radius ** 2
def circumference(self) -> float:
return 2 * self.PI * self.radius
def scale(self, factor: float) -> None:
"""Method that modifies instance state"""
self.radius *= factor
def __repr__(self) -> str:
return f"Circle(radius={self.radius})"
c = Circle(5)
print(f"Area: {c.area():.2f}") # Area: 78.54
print(f"Circumference: {c.circumference():.2f}") # Circumference: 31.42
c.scale(2)
print(c) # Circle(radius=10)
@classmethod: Class Methods
Class methods use the @classmethod decorator and receive cls (the class itself) instead of self as the first parameter. They are used for class-level operations rather than instance-level ones.
class Date:
def __init__(self, year: int, month: int, day: int):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_string: str) -> "Date":
"""Create a Date from a 'YYYY-MM-DD' string — alternative constructor"""
year, month, day = map(int, date_string.split("-"))
return cls(year, month, day)
@classmethod
def from_timestamp(cls, timestamp: float) -> "Date":
"""Create a Date from a Unix timestamp"""
import time
t = time.gmtime(timestamp)
return cls(t.tm_year, t.tm_mon, t.tm_mday)
@classmethod
def today(cls) -> "Date":
"""Create a Date for today"""
from datetime import date
d = date.today()
return cls(d.year, d.month, d.day)
def is_leap_year(self) -> bool:
return (self.year % 4 == 0 and self.year % 100 != 0) or (self.year % 400 == 0)
def __repr__(self) -> str:
return f"Date({self.year}-{self.month:02d}-{self.day:02d})"
# Creating objects in various ways
d1 = Date(2024, 3, 15)
d2 = Date.from_string("2024-06-20")
d3 = Date.today()
print(d1) # Date(2024-03-15)
print(d2) # Date(2024-06-20)
print(d3) # Date(2026-03-16) (today's date)
print(d1.is_leap_year()) # True
Class Methods and Inheritance
When class methods are inherited, cls automatically refers to the subclass, producing the correct type of object.
class Animal:
def __init__(self, name: str, sound: str):
self.name = name
self.sound = sound
@classmethod
def create_silent(cls, name: str) -> "Animal":
return cls(name, "...") # cls is the class that was called
def speak(self) -> str:
return f"{self.name}: {self.sound}"
class Dog(Animal):
def fetch(self) -> str:
return f"{self.name} fetches the ball!"
# Dog.create_silent() returns a Dog instance
silent_dog = Dog.create_silent("Quiet Dog")
print(type(silent_dog)) # <class '__main__.Dog'>
print(silent_dog.speak()) # Quiet Dog: ...
print(silent_dog.fetch()) # Quiet Dog fetches the ball!
@staticmethod: Static Methods
Static methods use the @staticmethod decorator and receive neither self nor cls. They are used for utility functions that are logically related to the class but do not depend on class or instance state.
class MathUtils:
@staticmethod
def is_prime(n: int) -> bool:
"""Check if a number is prime — no class/instance state needed"""
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
@staticmethod
def factorial(n: int) -> int:
"""Calculate factorial"""
if n < 0:
raise ValueError("Factorial is not defined for negative numbers.")
result = 1
for i in range(2, n + 1):
result *= i
return result
@staticmethod
def gcd(a: int, b: int) -> int:
"""Greatest common divisor using Euclidean algorithm"""
while b:
a, b = b, a % b
return a
# Can be called directly without an instance
print(MathUtils.is_prime(17)) # True
print(MathUtils.factorial(5)) # 120
print(MathUtils.gcd(48, 18)) # 6
# Can also be called on an instance (not recommended)
mu = MathUtils()
print(mu.is_prime(13)) # True
Choosing Between the Three Method Types
| Method Type | First Parameter | Instance State | Class State | When to Use |
|---|---|---|---|---|
| Instance method | self | Read/write | Read/write | When working with instance data |
| Class method | cls | None | Read/write | Alternative constructors, managing class state |
| Static method | None | None | None | Utility functions, pure computations |
class Temperature:
"""Temperature conversion class — uses all three method types"""
_conversion_count = 0 # Track number of conversions
def __init__(self, celsius: float):
self._celsius = celsius
# Instance method: depends on instance state (self._celsius)
def to_fahrenheit(self) -> float:
Temperature._conversion_count += 1
return self._celsius * 9/5 + 32
def to_kelvin(self) -> float:
Temperature._conversion_count += 1
return self._celsius + 273.15
# Class method: accesses class state
@classmethod
def get_conversion_count(cls) -> int:
return cls._conversion_count
@classmethod
def from_fahrenheit(cls, fahrenheit: float) -> "Temperature":
"""Alternative constructor: create Temperature from Fahrenheit"""
celsius = (fahrenheit - 32) * 5/9
return cls(celsius)
@classmethod
def from_kelvin(cls, kelvin: float) -> "Temperature":
"""Alternative constructor: create Temperature from Kelvin"""
return cls(kelvin - 273.15)
# Static method: pure computation, independent of class/instance
@staticmethod
def celsius_to_fahrenheit(celsius: float) -> float:
"""Pure conversion function — no state needed"""
return celsius * 9/5 + 32
@staticmethod
def is_valid_celsius(celsius: float) -> bool:
"""Check if temperature is above absolute zero"""
return celsius >= -273.15
def __repr__(self) -> str:
return f"Temperature({self._celsius}°C)"
# Various usage patterns
t1 = Temperature(100)
print(t1.to_fahrenheit()) # 212.0
print(t1.to_kelvin()) # 373.15
t2 = Temperature.from_fahrenheit(98.6)
print(t2) # Temperature(37.0°C)
t3 = Temperature.from_kelvin(0)
print(t3) # Temperature(-273.15°C)
# Static methods: used directly without an instance
print(Temperature.celsius_to_fahrenheit(25)) # 77.0
print(Temperature.is_valid_celsius(-300)) # False
# Class method: check conversion count
print(Temperature.get_conversion_count()) # 2
Practical Example: Date Parser
from datetime import datetime
from typing import Optional
import re
class DateParser:
"""A class for parsing date strings in various formats"""
SUPPORTED_FORMATS = [
"%Y-%m-%d",
"%Y/%m/%d",
"%d-%m-%Y",
"%d/%m/%Y",
"%Y%m%d",
"%B %d, %Y", # January 15, 2024
"%b %d, %Y", # Jan 15, 2024
]
def __init__(self, date: datetime):
self._date = date
@classmethod
def parse(cls, date_str: str) -> "DateParser":
"""Parse a date string in any supported format"""
date_str = date_str.strip()
for fmt in cls.SUPPORTED_FORMATS:
try:
dt = datetime.strptime(date_str, fmt)
return cls(dt)
except ValueError:
continue
raise ValueError(f"Unsupported date format: {date_str!r}")
@classmethod
def from_iso(cls, iso_str: str) -> "DateParser":
"""Parse an ISO 8601 format string"""
dt = datetime.fromisoformat(iso_str)
return cls(dt)
@classmethod
def now(cls) -> "DateParser":
"""Create with the current time"""
return cls(datetime.now())
# Static methods: pure utilities
@staticmethod
def is_valid_date(year: int, month: int, day: int) -> bool:
"""Check if a given date is valid"""
try:
datetime(year, month, day)
return True
except ValueError:
return False
@staticmethod
def guess_format(date_str: str) -> Optional[str]:
"""Guess the format of a date string"""
patterns = {
r"\d{4}-\d{2}-\d{2}$": "YYYY-MM-DD",
r"\d{4}/\d{2}/\d{2}$": "YYYY/MM/DD",
r"\d{8}$": "YYYYMMDD",
}
for pattern, fmt_name in patterns.items():
if re.match(pattern, date_str.strip()):
return fmt_name
return None
# Instance methods: depend on instance state
def format(self, fmt: str = "%B %d, %Y") -> str:
return self._date.strftime(fmt)
def days_until(self, other: "DateParser") -> int:
return (other._date - self._date).days
def is_weekend(self) -> bool:
return self._date.weekday() >= 5
def __repr__(self) -> str:
return f"DateParser({self._date.isoformat()})"
# Parsing various formats
d1 = DateParser.parse("2024-03-15")
d2 = DateParser.parse("15/03/2024")
d3 = DateParser.parse("March 15, 2024")
d4 = DateParser.now()
print(d1.format()) # March 15, 2024
print(d2.format("%Y/%m/%d")) # 2024/03/15
print(d3.is_weekend()) # False (Friday)
print(DateParser.is_valid_date(2024, 2, 29)) # True (2024 is a leap year)
print(DateParser.is_valid_date(2023, 2, 29)) # False
print(DateParser.guess_format("2024-03-15")) # YYYY-MM-DD
Practical Example: Unit Converter
class UnitConverter:
"""Utility class for unit conversions"""
# Class variable: conversion ratio tables
_length_to_meter = {
"mm": 0.001,
"cm": 0.01,
"m": 1.0,
"km": 1000.0,
"inch": 0.0254,
"foot": 0.3048,
"yard": 0.9144,
"mile": 1609.344,
}
_weight_to_kg = {
"mg": 0.000001,
"g": 0.001,
"kg": 1.0,
"t": 1000.0,
"oz": 0.028350,
"lb": 0.453592,
}
def __init__(self, value: float, unit: str):
self._value = value
self._unit = unit.lower()
@classmethod
def from_length(cls, value: float, unit: str) -> "UnitConverter":
if unit.lower() not in cls._length_to_meter:
raise ValueError(f"Unknown length unit: {unit}")
return cls(value, unit)
@classmethod
def from_weight(cls, value: float, unit: str) -> "UnitConverter":
if unit.lower() not in cls._weight_to_kg:
raise ValueError(f"Unknown weight unit: {unit}")
return cls(value, unit)
@staticmethod
def list_length_units() -> list[str]:
return list(UnitConverter._length_to_meter.keys())
@staticmethod
def list_weight_units() -> list[str]:
return list(UnitConverter._weight_to_kg.keys())
def to_length(self, target_unit: str) -> float:
if self._unit not in self._length_to_meter:
raise ValueError(f"{self._unit} is not a length unit.")
target_unit = target_unit.lower()
if target_unit not in self._length_to_meter:
raise ValueError(f"Unknown target unit: {target_unit}")
# Current unit → meters → target unit
in_meters = self._value * self._length_to_meter[self._unit]
return in_meters / self._length_to_meter[target_unit]
def to_weight(self, target_unit: str) -> float:
if self._unit not in self._weight_to_kg:
raise ValueError(f"{self._unit} is not a weight unit.")
target_unit = target_unit.lower()
if target_unit not in self._weight_to_kg:
raise ValueError(f"Unknown target unit: {target_unit}")
in_kg = self._value * self._weight_to_kg[self._unit]
return in_kg / self._weight_to_kg[target_unit]
def __repr__(self) -> str:
return f"UnitConverter({self._value} {self._unit})"
# Usage example
length = UnitConverter.from_length(100, "cm")
print(f"100 cm = {length.to_length('m')} m") # 1.0 m
print(f"100 cm = {length.to_length('inch'):.4f} in") # 39.3701 in
print(f"100 cm = {length.to_length('foot'):.4f} ft") # 3.2808 ft
weight = UnitConverter.from_weight(70, "kg")
print(f"70 kg = {weight.to_weight('lb'):.2f} lb") # 154.32 lb
print("Supported length units:", UnitConverter.list_length_units())
Expert Tips
1. Registry Pattern with Class Methods
class Serializer:
_registry: dict[str, type] = {}
def __init_subclass__(cls, format_name: str = "", **kwargs):
super().__init_subclass__(**kwargs)
if format_name:
Serializer._registry[format_name] = cls
@classmethod
def get(cls, format_name: str) -> type:
if format_name not in cls._registry:
raise KeyError(f"Unregistered format: {format_name}")
return cls._registry[format_name]
def serialize(self, data: dict) -> str:
raise NotImplementedError
class JSONSerializer(Serializer, format_name="json"):
def serialize(self, data: dict) -> str:
import json
return json.dumps(data, ensure_ascii=False)
class CSVSerializer(Serializer, format_name="csv"):
def serialize(self, data: dict) -> str:
return ",".join(f"{k}={v}" for k, v in data.items())
data = {"name": "Python", "version": "3.12"}
for fmt in ["json", "csv"]:
s = Serializer.get(fmt)()
print(f"{fmt}: {s.serialize(data)}")
2. Use @staticmethod only when there is a logical connection to the class
# Bad: unrelated utility function turned into a static method
class StringUtils:
@staticmethod
def reverse(s: str) -> str:
return s[::-1]
# Good: use static method only when logically related to the class
class EmailValidator:
DOMAIN_BLACKLIST = {"example.com", "test.com"}
@staticmethod
def is_valid_format(email: str) -> bool:
import re
return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email))
@classmethod
def is_allowed_domain(cls, email: str) -> bool:
domain = email.split("@")[-1].lower()
return domain not in cls.DOMAIN_BLACKLIST
def validate(self, email: str) -> tuple[bool, str]:
if not self.is_valid_format(email):
return False, "Invalid email format"
if not self.is_allowed_domain(email):
return False, "Domain not allowed"
return True, "Valid email"
Summary
- Instance method: accesses instance state via
self— used in most cases - Class method: operates at the class level via
cls— alternative constructors, factory pattern - Static method: utility function unrelated to class/instance — pure computation, helper functions
The key question for choosing a method type: "Does this method depend on instance state?" → Yes: instance method. "Does it depend on class state or return a class?" → Yes: class method. "Neither?" → Static method.