Python @staticmethod: Utility Functions That Belong in a Class Without Needing It
Python has three kinds of methods in a class: instance methods, class methods, and static methods. The first two are straightforward — instance methods get self, class methods get cls. Static methods get neither. They are regular functions that happen to live inside a class.
Why Put a Function Inside a Class at All?
If a static method is just a regular function, why not define it at module level? Sometimes that is the right choice. But there are cases where a function logically belongs with a class even though it does not touch any class or instance state:
- A validation function that only makes sense in the context of a particular class.
- A conversion or parsing utility that is always used with that class’s data.
- A helper that should be organised alongside related code, not scattered through a module.
Placing it as a @staticmethod signals to readers: “this function is related to this class, but it does not need an object or the class itself to work.”
Syntax
Apply @staticmethod above the method definition. The method takes no mandatory first parameter.
class TemperatureConverter: @staticmethod def celsius_to_fahrenheit(c): return (c * 9 / 5) + 32
@staticmethod def fahrenheit_to_celsius(f): return (f - 32) * 5 / 9
print(TemperatureConverter.celsius_to_fahrenheit(100)) # 212.0print(TemperatureConverter.fahrenheit_to_celsius(32)) # 0.0You can also call a static method on an instance, though calling it on the class is clearer:
conv = TemperatureConverter()print(conv.celsius_to_fahrenheit(37)) # 98.6 — works, but unusual styleA Realistic Example: Input Validation
import re
class User: def __init__(self, username, email): if not User.is_valid_username(username): raise ValueError(f"Invalid username: {username!r}") if not User.is_valid_email(email): raise ValueError(f"Invalid email: {email!r}") self.username = username self.email = email
@staticmethod def is_valid_username(username): """Username: 3-20 characters, letters, numbers, underscores only.""" return bool(re.match(r"^\w{3,20}$", username))
@staticmethod def is_valid_email(email): """Very basic email format check.""" return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email))
# Can validate without creating a User objectprint(User.is_valid_username("alice_99")) # Trueprint(User.is_valid_username("a")) # False — too shortprint(User.is_valid_email("bad-email")) # False
user = User("alice_99", "alice@example.com")print(user.username) # alice_99The validation methods do not need to know anything about an existing user. They just check a string against a rule. Making them @staticmethod means you can call them before creating an object — useful when you want to validate user input before you even try to build the object.
Comparing the Three Method Types
class PaymentProcessor: fee_rate = 0.025 # 2.5% platform fee
def __init__(self, merchant_id): self.merchant_id = merchant_id self.processed = []
def process(self, amount): """Instance method — needs self to record the transaction.""" net = amount * (1 - self.fee_rate) self.processed.append(net) return net
@classmethod def from_env(cls): """Class method — builds an instance using class-level knowledge.""" import os return cls(os.environ.get("MERCHANT_ID", "default-id"))
@staticmethod def format_currency(amount, symbol="£"): """Static method — pure utility, no class or instance needed.""" return f"{symbol}{amount:,.2f}"
p = PaymentProcessor("merchant-001")net = p.process(1000)
print(PaymentProcessor.format_currency(net)) # £975.00print(PaymentProcessor.format_currency(net, "$")) # $975.00process()is an instance method because it readsself.fee_rateand writes toself.processed.from_env()is a class method because it creates a new instance from environment config.format_currency()is a static method because it only does string formatting — it never touches the class or any object.
@staticmethod vs @classmethod
@staticmethod | @classmethod | |
|---|---|---|
| Receives | Nothing extra | cls (the class) |
| Access class state | No | Yes |
| Inheritance-aware | No | Yes — cls is the calling subclass |
| Typical use | Pure utilities, validators, converters | Factory methods, registry patterns |
The inheritance difference is significant. If you have a subclass:
class EuroConverter(TemperatureConverter): pass
# @staticmethod — subclass call works, but cls is irrelevantprint(EuroConverter.celsius_to_fahrenheit(0)) # 32.0
class Animal: @classmethod def create(cls): return cls() # returns a Dog or Cat depending on which calls it
class Dog(Animal): pass
d = Dog.create()print(type(d)) # <class '__main__.Dog'>A @classmethod’s cls correctly points to the calling subclass. A @staticmethod has no such awareness.
When Not to Use @staticmethod
Do not reach for @staticmethod just to avoid writing self. If a function genuinely needs to read or modify instance or class state, it should be an instance method or class method respectively. A static method that constantly accesses global state through module-level variables is a code smell — the class is not actually helping.
Also, if a function would be equally clear as a standalone module-level function, there is no reason to nest it in a class. Putting unrelated functions inside a class “for organisation” creates a class that has no coherent identity.
Summary
@staticmethodattaches a function to a class without giving itselforcls.- Use it for utilities, validators, and converters that logically belong with a class but do not need access to object or class state.
- It differs from
@classmethodin that it is not inheritance-aware. - If the function genuinely needs the class or the instance, use a regular or class method instead.