DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
Getting Started
01
What is Python and Why Learn It in 2026
02
Installing Python on Windows, Mac, and Linux
03
Setting up VS Code / PyCharm for Python
04
Understanding the Python Interpreter and REPL
05
Writing and Running Your First Python Program
06
Python Syntax and Indentation Rules
Variables and Data Types
01
Variables and Naming Conventions in Python
02
Numbers in Python (int, float, complex)
03
Strings and String Methods in Python
04
Booleans in Python
05
Type Conversion / Casting in Python
06
Understanding the None Type in Python
Operators
01
Arithmetic Operators in Python
02
Comparison Operators in Python
03
Logical Operators in Python
04
Assignment Operators in Python
05
Bitwise Operators in Python
06
Identity (is) and Membership (in) Operators
07
Walrus Operator (:=) in Python
Input, Output & Strings
01
input() and print() Functions in Python
02
f-strings and String Formatting in Python
03
String Slicing and Indexing in Python
04
Common String Methods (split, join, strip, replace)
05
Escape Characters in Python Strings
Control Flow
01
if, elif, else Statements in Python
02
Nested Conditionals in Python
03
for Loops in Python
04
while Loops in Python
05
break, continue, and pass Statements
06
Loop with else Clause in Python
07
match-case (Structural Pattern Matching)
Data Structures
01
Python Lists: Creation, Indexing, Slicing, Methods
02
Python Tuples and Immutability
03
Python Sets and Set Operations
04
Python Dictionaries: Keys, Values, and Methods
05
List Comprehensions in Python
06
Dictionary & Set Comprehensions in Python
07
Working with Nested Lists and Dictionaries
Functions
01
Defining and Calling Functions in Python
02
Function Arguments: Positional, Keyword, Default
03
*args and **kwargs in Python
04
Return Statements in Python Functions
05
Lambda Functions in Python
06
Recursion in Python
07
Variable Scope: Local, Global, Nonlocal
08
Docstrings and Function Annotations
Functional Programming Basics
01
map(), filter(), and reduce() in Python
02
Closures in Python Explained
03
Python Decorators: Basic to Advanced
04
Generators and yield in Python
Object-Oriented Programming (OOP)
01
Classes and Objects in Python
02
The init Constructor in Python
03
Instance vs Class Variables in Python
04
Inheritance in Python (Single, Multiple, Multilevel)
05
Polymorphism in Python
06
Encapsulation in Python
07
Abstraction in Python with the abc Module
08
Magic/Dunder Methods in Python
09
Static and Class Methods in Python
10
The @property Decorator in Python
11
Python Dataclasses Explained
Error Handling
01
try, except, finally in Python
02
Handling Multiple Exceptions in Python
03
Raising Custom Exceptions in Python
04
Understanding Python's Exception Hierarchy
05
Assertions in Python
File Handling
01
Reading and Writing Text Files in Python
02
Working with the with Statement in Python
03
Working with CSV Files in Python
04
Working with JSON Files in Python
05
Working with Directories (os, pathlib) in Python
Modules and Packages
01
Importing Built-in Modules in Python
02
Creating Your Own Modules in Python
03
Understanding Packages and init.py
04
Python Standard Library Overview
05
Installing Packages with pip
Advanced Core Concepts
01
Iterators and Iterables in Python
02
Context Managers in Python (with, contextlib)
03
Multithreading Basics in Python
04
Multiprocessing Basics in Python
05
Asyncio Basics: async/await in Python
06
Memory Management and Garbage Collection in Python
07
Type Hints in Python (typing module)

Static and Class Methods in Python

Every method inside a Python class falls into one of three categories, each expressing a genuinely different relationship to the class it belongs to. This article covers the python class method (@classmethod) and python static method (@staticmethod) decorators alongside the familiar default — instance methods — and gives you a clear, practical way to decide which one fits a given method.

Introduction: Three Kinds of Methods, Three Relationships

Python classes support three kinds of methods: instance methods (the default, no decorator needed), class methods (@classmethod), and static methods (@staticmethod). Each one expresses a different relationship between the method and the class it's defined on.

The key question to ask

When writing a method, ask yourself: does it need self (a specific instance's data)? Does it need cls (the class itself, but not any particular instance)? Or does it need neither?

A quick preview table

Method type

First parameter

Access to

Instance method

self

The specific instance, and through it, the class

Class method

cls

The class itself, not any specific instance

Static method

(none)

Neither — essentially a plain function

The rest of this article walks through each in detail, then closes with a practical decision guide.

Instance Methods: The Default

The familiar pattern

This is the pattern you've used throughout every earlier article in this series involving classes: a method whose first parameter is self, giving it access to that specific object's data (and, through the instance, the class it belongs to as well):

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def greet(self):
        return f"Hi, I'm {self.name} and I'm {self.age} years old"

alex = Person("Alex", 30)
print(alex.greet())   # Hi, I'm Alex and I'm 30 years old

greet() reads self.name and self.age, which only exist on a specific instance — this is exactly the situation instance methods are built for.

Why this is the right choice for per-object data

Any time a method genuinely needs to read or modify data unique to a specific object — as covered in the earlier instance vs. class variables article — an instance method, with self as its first parameter, is the correct and default choice. This covers the overwhelming majority of methods you'll write on any given class.

Class Methods with @classmethod

First parameter: cls

A class method's first parameter is conventionally named cls, and it refers to the class itself — not any specific instance. Class methods can access class-level data (and the class itself, including its other class methods), but they don't automatically have access to any particular object's instance attributes.

class Person:
    population = 0

    def __init__(self, name):
        self.name = name
        Person.population += 1

    @classmethod
    def get_population(cls):
        return cls.population

Person("Alex")
Person("Sam")
print(Person.get_population())   # 2

get_population() never needs self, since it's not reading or modifying anything specific to one instance — it's operating purely on class-level, shared data.

The most common real-world use: alternative constructors

This is genuinely the pattern you'll encounter most often for @classmethod in real code: building and returning an instance in a way that's different from the standard __init__ signature — often called a factory method or alternative constructor:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def from_string(cls, person_string):
        name, age = person_string.split(",")
        return cls(name.strip(), int(age.strip()))

alex = Person.from_string("Alex, 30")
print(alex.name, alex.age)   # Alex 30

from_string() offers a genuinely different way to construct a Person — from a single formatted string, rather than separate name and age arguments — without needing to cram that parsing logic into __init__ itself, which would make __init__ messier and less focused, exactly the guidance given in the earlier __init__ article about keeping constructors lightweight.

The critical inheritance detail: cls(...), not the hardcoded class name
This is genuinely important, and easy to get subtly wrong: inside a classmethod, always construct the new instance using cls(...), never by hardcoding the literal class name directly. Using cls(...) ensures that subclasses correctly get an instance of themselves, not of the base class:
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def from_string(cls, person_string):
        name, age = person_string.split(",")
        return cls(name.strip(), int(age.strip()))   # correct — uses cls

class Employee(Person):
    def __init__(self, name, age, employee_id):
        super().__init__(name, age)
        self.employee_id = employee_id

# Even though from_string() is inherited from Person, calling it on Employee
# correctly produces an Employee instance, because it uses cls(...) internally

If from_string() had hardcoded return Person(...) instead of return cls(...), calling Employee.from_string(...) would incorrectly return a plain Person object — silently discarding the fact that it was called on the Employee subclass, and losing access to anything Employee adds on top. Using cls is what makes factory methods correctly inheritance-aware.

Static Methods with @staticmethod

No automatic first parameter at all

A static method receives no automatic first parameter — no self, no cls. It's essentially a plain, ordinary function that happens to be namespaced inside a class for organizational purposes, and it has no automatic access to either instance or class data:

class MathUtils:
    @staticmethod
    def add(a, b):
        return a + b

print(MathUtils.add(3, 5))   # 8 — called without needing an instance at all
Good use cases

Static methods fit utility or helper logic that conceptually belongs with a class — thematically related to what the class represents — but genuinely doesn't need any instance or class state to do its job:

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    @staticmethod
    def celsius_to_fahrenheit(celsius):
        return celsius * 9 / 5 + 32

    @staticmethod
    def is_valid_temperature(celsius):
        return celsius >= -273.15   # absolute zero

print(Temperature.celsius_to_fahrenheit(100))   # 212.0
print(Temperature.is_valid_temperature(-300))    # False

Both of these methods are thematically tied to temperature conversion and validation — a natural fit to live inside Temperature — but neither needs self.celsius or any other instance-specific state to compute their result; they operate purely on whatever arguments are passed in directly.

The benefit beyond organization

Marking a method @staticmethod isn't purely about tidiness — it communicates something concrete to anyone reading the code: calling this method is guaranteed not to touch any object's state, since it has no automatic access to self or cls at all. This is a genuinely useful signal when scanning through a class's methods trying to understand what might or might not have side effects on a specific object. It also makes the method trivially easy to test in complete isolation — no need to construct an instance of the class at all just to verify the static method's behavior:

# Testing celsius_to_fahrenheit requires zero setup — no Temperature instance needed
assert Temperature.celsius_to_fahrenheit(0) == 32
assert Temperature.celsius_to_fahrenheit(100) == 212

Choosing the Right One: A Practical Decision Guide

A simple heuristic
  • Needs self (reads or modifies a specific instance's data) → instance method.

  • Needs cls, or specifically needs to create and return a new instance of the class → class method.

  • Needs neither → static method — or consider whether it might make more sense as a plain, module-level function instead, if it's genuinely general-purpose and doesn't have any strong conceptual tie to the class at all.

A common mistake: using @staticmethod for factory methods

This is worth calling out explicitly, since it's a genuinely easy mistake to make: using @staticmethod for something that's really a factory-style constructor, rather than @classmethod:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @staticmethod
    def from_string(person_string):   # should be @classmethod, not @staticmethod
        name, age = person_string.split(",")
        return Person(name.strip(), int(age.strip()))   # hardcoded class name — breaks for subclasses

This version works correctly when called directly on Person, but it breaks the exact inheritance scenario covered in Section 3 — calling Employee.from_string(...) would still incorrectly return a plain Person, since there's no cls available to reference the actual calling class. Any factory-style method meant to construct and return an instance should almost always be a @classmethod, using cls(...), specifically so it continues to behave correctly across any future subclasses.

A worked example combining all three
import re

class User:
    total_users = 0

    def __init__(self, username, email):
        self.username = username
        self.email = email
        User.total_users += 1

    def greet(self):   # instance method — needs self
        return f"Welcome, {self.username}!"

    @classmethod
    def create_admin(cls, username):   # class method — creates a new instance via cls
        return cls(username, f"{username}@admin.example.com")

    @staticmethod
    def validate_email(email):   # static method — no instance or class data needed
        return bool(re.match(r"^[^@]+@[^@]+\.[^@]+$", email))

user = User("alex99", "[email protected]")
print(user.greet())   # Welcome, alex99!

admin = User.create_admin("root")
print(admin.email)   # [email protected]

print(User.validate_email("not-an-email"))   # False
print(User.total_users)   # 2

This single class demonstrates the full range covered in this article: greet() needs self to reference the specific user's name, create_admin() needs cls to correctly build and return a new instance (correctly, even for any future subclasses of User), and validate_email() needs neither — it's a pure, standalone piece of logic that just happens to belong conceptually alongside the rest of the User class's responsibilities.

PREVIOUSNEXT LESSON