
Picture this: you're three hours into building a data pipeline and something subtle is wrong. Your class is initializing objects just fine, your methods look correct, and yet the logic keeps misbehaving. You stare at the code, move things around, add print statements. Then, finally, you realize the problem, you used a regular instance method where you needed a class method, so every call is modifying a single object's state instead of shared class-level data. The fix is a three-line change, but it took three hours to find because you didn't have a clear mental model of what each method type actually does.
This happens more than you'd think. Python's three method types, instance, class, and static, look almost identical on the surface. They're all defined inside a class body, they're all called with dot notation, and they all execute code. The differences are subtle but the consequences are not. Get them confused and you end up with state-management bugs that only appear at scale, factory methods that return the wrong type in a subclass, or utility functions that are unnecessarily bound to object instances. More importantly, when you're reading professional codebases, Django source code, scikit-learn internals, FastAPI routing, you'll encounter all three types constantly. If you can't read them fluently, you're going to be guessing at what the code does.
We're going to untangle this completely. You'll understand not just how each method type works mechanically, but why Python has three types at all, what self and cls really mean under the hood, and how to make the right choice every time. By the end, you'll know exactly when to reach for instance methods, class methods, and static methods, and why that matters for building maintainable, professional code.
Table of Contents
- The Three Methods: Quick Comparison
- Why Three Method Types Exist
- Self and Cls Demystified
- Instance Methods: The Default
- Multiple Instances, Independent Data
- Instance Methods and Method Chaining
- Class Methods: Working with the Class Itself
- Alternate Constructors: The Real Power of @classmethod
- Class Variables and Shared State
- Static Methods: Utility Functions in a Namespace
- When to Use Static Methods
- A More Realistic Example: Data Processing
- When to Use Each Type
- Common Method Mistakes
- Decision Guide: Which Method Should You Use?
- Use Instance Methods When:
- Use Class Methods When:
- Use Static Methods When:
- Unbound Methods and Access from the Class
- Method Resolution Order and Inheritance
- Real-World Patterns
- Pattern 1: JSON Configuration with Alternate Constructors
- Pattern 2: Singleton Pattern with Class Methods
- Pattern 3: Fluent API with Method Chaining
- Summary
The Three Methods: Quick Comparison
Before we go deep, here's the lay of the land:
| Method Type | First Parameter | Access | Common Use |
|---|---|---|---|
| Instance | self | State + class | Operate on specific objects |
| Class | cls | Class definition | Alternate constructors, factory methods |
| Static | None | Neither | Utility functions grouped with class |
Notice the pattern? The first parameter tells you everything. self means you're working with one specific object. cls means you're working with the class itself. No first parameter? You're just borrowing the class as a namespace.
Why Three Method Types Exist
Before you can use these types with confidence, it helps to understand the problem they each solve. Python didn't invent three method types because the language designers liked complexity, each type exists to handle a fundamentally different relationship between a function and the data it operates on.
Object-oriented programming is built around the idea that code and data belong together. When you define a class, you're creating a blueprint that bundles state (attributes) with behavior (methods). But "belonging together" can mean different things. Sometimes a behavior operates on a specific object's unique data, that's the job of an instance method. Sometimes a behavior operates on data shared across all instances, or on the class blueprint itself, that's a class method. And sometimes a behavior is simply related to a concept but doesn't need any object or class data at all, that's a static method.
Think about a User class. The method send_welcome_email(self) needs to know this specific user's email address, it's an instance method. The method from_database_row(cls, row) needs to create a new User from raw data without an existing instance, it's a class method acting as an alternate constructor. The method is_valid_email_format(email) just checks whether a string looks like an email, it doesn't care about any particular user, so it's a static method. Three different relationships, three different tools. When you see it this way, the choice becomes obvious rather than arbitrary.
Self and Cls Demystified
Here's the thing about self and cls that often trips people up: they are not Python keywords. You could name them anything, this, instance, klass, whatever you want. The names self and cls are just conventions so strong that breaking them would make your code look alien to every other Python developer. What makes them special isn't the name, it's their position and how Python passes them automatically.
When you call user.greet(), Python translates that under the hood into User.greet(user). The instance on the left side of the dot gets passed as the first argument to the function. Python handles this translation automatically every time you call a method on an instance. That's self, it's just "the object you called this method on," delivered automatically.
cls works the same way but for class methods. When you call User.from_dict(data), Python translates it to something like User.from_dict(User, data). The class itself gets passed as the first argument. The critical difference from self is that cls is always the class the method was called on, not necessarily the class where the method was defined. This is what makes class methods so powerful for inheritance: if AdminUser inherits from_dict from User and you call AdminUser.from_dict(data), then cls inside that method is AdminUser, not User. Python hands you exactly the class you need to create instances of the right type. Once you internalize this automatic passing behavior, the three method types click into place naturally.
Instance Methods: The Default
An instance method is what you've probably been writing without thinking about it:
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def greet(self):
return f"Hello, I'm {self.name}!"
def change_email(self, new_email):
self.email = new_email
print(f"Updated email to {new_email}")
# Create an instance
user = User("Alice", "alice@example.com")
# Call the method on the instance
print(user.greet()) # "Hello, I'm Alice!"
user.change_email("alice.new@example.com")Expected output:
Hello, I'm Alice!
Updated email to alice.new@example.com
See what happened? When you called user.greet(), Python automatically passed user as the first argument (self). You didn't write it, but it was there. Same with change_email, the instance itself gets passed implicitly. This automatic passing is what distinguishes a method call from a plain function call, and it's what gives instance methods their superpower: they always know which specific object they belong to.
Here's the hidden layer: instance methods are the workhorses of OOP. They let each object maintain its own state and perform operations specific to that state. The User object user has different data than some other User object you might create, and the instance method sees exactly what it needs. Every time you're thinking "this behavior needs to read or change something about a specific object," an instance method is your answer.
Multiple Instances, Independent Data
This is crucial to understand:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
return f"Deposited ${amount}. New balance: ${self.balance}"
def withdraw(self, amount):
if amount <= self.balance:
self.balance -= amount
return f"Withdrew ${amount}. New balance: ${self.balance}"
return "Insufficient funds"
# Two accounts, two instances
account1 = BankAccount("Alice", 500)
account2 = BankAccount("Bob", 300)
print(account1.deposit(100)) # Alice gets $600
print(account2.deposit(100)) # Bob gets $400
print(account1.balance) # 600
print(account2.balance) # 400Expected output:
Deposited $100. New balance: $600
Deposited $100. New balance: $400
600
400
Each instance (account1, account2) has its own balance. When you call account1.deposit(100), it modifies account1.balance, not account2.balance. The self parameter is what makes this possible, it always refers to the specific instance the method was called on. This isolation is the entire point of object-oriented programming: you can have a hundred BankAccount objects, and each one tracks its own state independently without interfering with any other.
Instance Methods and Method Chaining
Some APIs use method chaining, calling multiple methods in a row. This is possible because instance methods can return self:
class StringBuilder:
def __init__(self):
self.content = ""
def append(self, text):
self.content += text
return self # Return the instance itself
def append_line(self, text):
self.content += text + "\n"
return self
def build(self):
return self.content
# Chain methods together
result = (StringBuilder()
.append("Hello ")
.append("World")
.append_line("!")
.append("How are you?")
.build())
print(result)Expected output:
Hello World!
How are you?
By returning self, each method passes the same instance to the next call. This is a builder pattern, a clean way to construct complex objects step by step. You'll recognize this pattern in libraries like SQLAlchemy's query builder, Django's ORM, and pandas' method chaining API. The trick is always the same: return self to make the next call available on the same object.
Class Methods: Working with the Class Itself
Now let's shift gears. A class method receives the class, not an instance:
class Config:
environment = "development" # Class variable
@classmethod
def set_environment(cls, env):
cls.environment = env
@classmethod
def get_environment(cls):
return cls.environment
# Call on the class directly (no instance needed)
print(Config.get_environment()) # "development"
Config.set_environment("production")
print(Config.get_environment()) # "production"
# You can also call on an instance (but not common)
config_instance = Config()
print(config_instance.get_environment()) # "production"Expected output:
development
production
production
Notice: we called Config.set_environment() on the class itself, not on an instance. The @classmethod decorator tells Python to pass the class (cls) as the first argument, not an instance. This is how you modify data that belongs to the class, not to any specific object, but to the template itself. When you change cls.environment, that change is reflected everywhere, in every instance, because they all share the same class-level variable.
Alternate Constructors: The Real Power of @classmethod
Here's where class methods shine: alternate constructors. Instead of overloading __init__ (which Python doesn't support), you create factory methods:
from datetime import datetime
class User:
def __init__(self, name, email, created_at=None):
self.name = name
self.email = email
self.created_at = created_at or datetime.now()
@classmethod
def from_dict(cls, data):
"""Create a User from a dictionary."""
return cls(
name=data["name"],
email=data["email"],
created_at=data.get("created_at")
)
@classmethod
def from_csv_line(cls, line):
"""Create a User from a CSV line (name,email)."""
name, email = line.strip().split(",")
return cls(name=name, email=email)
def __repr__(self):
return f"User(name={self.name}, email={self.email})"
# Create from dict
user_dict = {"name": "Alice", "email": "alice@example.com"}
user1 = User.from_dict(user_dict)
print(user1)
# Create from CSV line
csv_line = "Bob,bob@example.com"
user2 = User.from_csv_line(csv_line)
print(user2)
# Traditional constructor still works
user3 = User("Charlie", "charlie@example.com")
print(user3)Expected output:
User(name=Alice, email=alice@example.com)
User(name=Bob, email=bob@example.com)
User(name=Charlie, email=charlie@example.com)
The key insight here is that from_dict and from_csv_line use cls(...) instead of User(...) to create instances. This single choice is what makes class methods so much more powerful than plain class-level functions. Why is this better than just defining regular functions outside the class? Three reasons:
- Polymorphism: If you subclass
User, the subclass automatically inherits these factory methods, and they create instances of the subclass, not the parent class. - Clarity:
User.from_dict()is more readable thancreate_user_from_dict(). - Namespace: The method belongs to the class, making your API more organized.
Let me show you the polymorphism benefit:
class AdminUser(User):
def __init__(self, name, email, permission_level=0, created_at=None):
super().__init__(name, email, created_at)
self.permission_level = permission_level
@classmethod
def from_dict(cls, data):
"""Override to handle permission_level."""
return cls(
name=data["name"],
email=data["email"],
permission_level=data.get("permission_level", 0),
created_at=data.get("created_at")
)
# When you call from_dict on AdminUser, you get an AdminUser
admin_data = {"name": "Admin", "email": "admin@example.com", "permission_level": 5}
admin = AdminUser.from_dict(admin_data)
print(type(admin)) # <class '__main__.AdminUser'>
print(admin.permission_level) # 5Expected output:
<class '__main__.AdminUser'>
5
The key insight: cls is always the class on which the method was called. If you call User.from_dict(), cls is User. If you call AdminUser.from_dict(), cls is AdminUser. This polymorphic behavior is what separates class methods from simple functions, they participate in the inheritance hierarchy automatically, making your code extensible without modification.
Class Variables and Shared State
Class methods are perfect for working with class variables, data shared across all instances:
class BankAccount:
account_count = 0 # Class variable
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
BankAccount.account_count += 1
@classmethod
def total_accounts(cls):
return cls.account_count
@classmethod
def reset_count(cls):
cls.account_count = 0
account1 = BankAccount("Alice", 500)
account2 = BankAccount("Bob", 300)
account3 = BankAccount("Charlie", 700)
print(f"Total accounts: {BankAccount.total_accounts()}") # 3
BankAccount.reset_count()
print(f"After reset: {BankAccount.total_accounts()}") # 0Expected output:
Total accounts: 3
After reset: 0
The account_count variable lives on the class, not on any instance. Every time __init__ runs, it increments this shared counter. The class method total_accounts reads it back through cls, and reset_count modifies it through cls. Note: you can modify cls.account_count directly in a class method because cls gives you access to the class definition itself. This pattern is how you track global state tied to a concept, things like "how many connections are open," "how many users have been created," or "what is the current configuration."
Static Methods: Utility Functions in a Namespace
A static method is... honestly, just a function that lives inside a class. It doesn't get self or cls:
class MathUtils:
@staticmethod
def add(a, b):
return a + b
@staticmethod
def multiply(a, b):
return a * b
# Call on the class (no instance needed)
print(MathUtils.add(5, 3)) # 8
print(MathUtils.multiply(4, 7)) # 28
# You can also call on an instance (unusual, but works)
utils = MathUtils()
print(utils.add(10, 2)) # 12Expected output:
8
28
12
The @staticmethod decorator tells Python not to inject any automatic first argument. What you write is exactly what gets called. There's no hidden self, no hidden cls, nothing extra. This means static methods have no implicit access to instance data or class data, they operate purely on the arguments you pass them.
When to Use Static Methods
Static methods are for utility functions that are logically related to a class but don't need access to instance or class state. Example:
class DataValidator:
@staticmethod
def is_valid_email(email):
"""Check if email looks valid."""
return "@" in email and "." in email.split("@")[-1]
@staticmethod
def is_valid_phone(phone):
"""Check if phone is 10 digits."""
return len(phone.replace("-", "").replace(" ", "")) == 10
@staticmethod
def is_valid_zipcode(zipcode):
"""Check if zipcode is 5 digits."""
return len(zipcode) == 5 and zipcode.isdigit()
# Use them
print(DataValidator.is_valid_email("alice@example.com")) # True
print(DataValidator.is_valid_email("invalid.email")) # False
print(DataValidator.is_valid_phone("555-123-4567")) # True
print(DataValidator.is_valid_zipcode("12345")) # TrueExpected output:
True
False
True
True
These validation functions don't need to know about any specific instance or modify the class. They're grouped in a class for organization, so you know all validation logic lives in DataValidator. When another developer comes into your codebase looking for email validation, they know exactly where to find it. The class acts as a label on a folder rather than a blueprint for objects.
Hidden layer: Static methods are a namespace organizer, not a true OOP mechanism. You could define these as module-level functions and it'd work the same way. But grouping them in a class makes your API cleaner, keeps related utilities together, and makes testing easier since you can mock the entire DataValidator class in tests.
A More Realistic Example: Data Processing
class DataProcessor:
@staticmethod
def parse_csv(csv_string):
"""Parse CSV into list of dicts."""
lines = csv_string.strip().split("\n")
headers = lines[0].split(",")
rows = []
for line in lines[1:]:
values = line.split(",")
row = dict(zip(headers, values))
rows.append(row)
return rows
@staticmethod
def filter_by_field(rows, field, value):
"""Filter rows by field value."""
return [row for row in rows if row.get(field) == value]
@staticmethod
def to_json_string(rows):
"""Convert rows to JSON."""
import json
return json.dumps(rows, indent=2)
csv_data = """name,age,city
Alice,28,NYC
Bob,35,LA
Charlie,28,NYC"""
rows = DataProcessor.parse_csv(csv_data)
ny_residents = DataProcessor.filter_by_field(rows, "city", "NYC")
json_output = DataProcessor.to_json_string(ny_residents)
print(json_output)Expected output:
[
{
"name": "Alice",
"age": "28",
"city": "NYC"
},
{
"name": "Charlie",
"age": "28",
"city": "NYC"
}
]
Static methods keep utility logic co-located with the concept it supports, without the overhead of instantiation. Notice that none of these three methods need to remember anything between calls, each one takes inputs, does a transformation, and returns a result. That stateless characteristic is your clearest signal that a static method is the right choice.
When to Use Each Type
Knowing the mechanics is one thing. Knowing when to reach for each type in the real world is what separates developers who write functional code from developers who write professional code. Here's a practical decision framework.
Reach for an instance method whenever the behavior you're writing needs to read or modify data that belongs to a specific object. If the method body would reference self.anything, it's an instance method. This covers the vast majority of what you'll write, object initialization behavior, state transitions, computed properties, object-to-object interactions. Ask yourself: "Does this operation need to know which specific object it's working on?" If yes, instance method.
Reach for a class method in three situations. First, alternate constructors: whenever you want to create instances from different data formats (from_json, from_csv, from_env, from_file), a class method is cleaner than a standalone function because it participates in inheritance. Second, shared state management: when you need to read or modify data that belongs to the class itself rather than any instance. Third, factory patterns: when you need logic that decides which subclass to instantiate based on input data. A @classmethod that inspects input and returns cls(...) or a subclass instance is a powerful pattern.
Reach for a static method when the function is genuinely stateless with respect to the class. Validation helpers, format converters, math utilities, string parsers, functions that take inputs and return outputs without touching any object or class data. The test is simple: if you removed the method from the class and made it a standalone function, would it work identically? If yes, it's a candidate for a static method. You'd use a static method over a module-level function mainly when you want the organizational benefit of grouping related utilities under a class namespace.
Common Method Mistakes
Even experienced developers make predictable mistakes with Python's three method types. Knowing these patterns in advance will save you hours of debugging.
The first mistake is using an instance method when you need a class method for a constructor. You'll see this as __init__ with a dozen optional parameters trying to handle every possible input format. The fix is extracting those branches into named class methods: from_dict, from_string, from_file. Each constructor gets a clear name, clear inputs, and clear responsibilities.
The second mistake is forgetting that cls in a class method is the class you called it on, not necessarily the class where it's defined. If you hard-code the class name inside a class method, writing User(...) instead of cls(...), you break inheritance. Subclasses will call your factory method and get back a User instead of the subclass. Always use cls(...) in class methods when you're creating instances.
The third mistake is using a static method when you need an instance method, or vice versa. This usually happens when you're unsure whether a helper "needs" the instance data. When in doubt, ask: could this ever need access to self? If there's even a chance, use an instance method. You can always ignore self inside an instance method, but you can't access it from a static method without restructuring the code.
The fourth mistake is calling an instance method on the class itself and being surprised that it fails. When you write User.greet() instead of user.greet(), Python doesn't know which instance to pass as self, so you get a TypeError about missing arguments. This is actually Python being helpful, it's telling you that you forgot to create an instance first. Always call instance methods on objects, not on classes.
The fifth mistake is overusing static methods as a way to avoid thinking about design. Static methods are fine for genuine utilities, but if you find yourself writing a dozen static methods that all operate on the same kind of data, consider whether that data should be an instance's state instead.
Decision Guide: Which Method Should You Use?
Here's the real decision tree:
Use Instance Methods When:
- You need to access or modify instance data (
self.attribute) - Each call operates on a specific object
- You're implementing object behavior (what instances do)
Example: user.change_email(), account.withdraw()
Use Class Methods When:
- You need to access or modify class data (
cls.variable) - You're creating alternate constructors (
from_dict,from_json) - You're implementing factory patterns
- You need to work with the class definition itself
Example: User.from_dict(), Config.set_environment()
Use Static Methods When:
- You need no access to instance or class state
- You're defining utility functions logically tied to a concept
- You want to organize related functions under a class namespace
Example: DataValidator.is_valid_email(), MathUtils.add()
Here's a complete example combining all three:
class Product:
total_products = 0 # Class variable
def __init__(self, name, price):
self.name = name
self.price = price
Product.total_products += 1
# Instance method: work with this specific product
def apply_discount(self, percent):
"""Apply a discount to this product."""
self.price = self.price * (1 - percent / 100)
return f"{self.name} now costs ${self.price:.2f}"
# Class method: create from data format
@classmethod
def from_json(cls, json_string):
"""Create a Product from JSON."""
import json
data = json.loads(json_string)
return cls(data["name"], data["price"])
# Class method: report on the class
@classmethod
def total_in_catalog(cls):
"""How many products have we created?"""
return cls.total_products
# Static method: utility function
@staticmethod
def calculate_tax(price, rate=0.08):
"""Calculate sales tax on a price."""
return price * rate
# Use them all
import json
# Create using instance constructor
product1 = Product("Laptop", 1000)
# Create using class method
json_data = '{"name": "Mouse", "price": 25}'
product2 = Product.from_json(json_data)
# Use instance method
print(product1.apply_discount(10)) # "$900.00"
# Use class method
print(f"Total products: {Product.total_in_catalog()}") # 2
# Use static method (on class or instance, doesn't matter)
print(f"Tax on laptop: ${Product.calculate_tax(900):.2f}") # "$72.00"Expected output:
Laptop now costs $900.00
Total products: 2
Tax on laptop: $72.00
This example shows all three working together in a coherent class. The instance method apply_discount modifies a specific product's price. The class method from_json creates new products without requiring an existing instance. The class method total_in_catalog reports on class-level shared data. The static method calculate_tax performs a calculation that's conceptually related to products but doesn't need any particular product's data.
Unbound Methods and Access from the Class
Here's something that trips people up: can you call an instance method on the class itself?
class Calculator:
def add(self, a, b):
return a + b
# Try calling on the class (not an instance)
try:
result = Calculator.add(5, 3)
except TypeError as e:
print(f"Error: {e}")
# Call on an instance (this works)
calc = Calculator()
result = calc.add(5, 3)
print(f"Result: {result}")
# You can also get the unbound method and call it manually
method = Calculator.add
result = method(calc, 5, 3) # Explicitly pass the instance
print(f"Manual call: {result}")Expected output:
Error: add() missing 1 required positional argument: 'a'
Result: 8
Manual call: 8
When you access an instance method via the class (not an instance), Python gives you the raw function. You have to pass self explicitly, it doesn't get passed automatically. This is almost never useful in practice, but it's good to know. It also clarifies why self is a convention rather than a keyword: from Python's perspective, calc.add(5, 3) and Calculator.add(calc, 5, 3) are completely equivalent. The dot-notation call on an instance is just syntactic sugar for the explicit version.
Method Resolution Order and Inheritance
When you combine instance, class, and static methods with inheritance, things get interesting:
class Animal:
species_count = 0
def __init__(self, name):
self.name = name
Animal.species_count += 1
def speak(self):
"""Instance method."""
return f"{self.name} makes a sound"
@classmethod
def species_total(cls):
"""Class method."""
return f"Total {cls.__name__}: {cls.species_count}"
@staticmethod
def is_four_legged(animal_type):
"""Static method."""
return animal_type in ["dog", "cat", "horse"]
class Dog(Animal):
species_count = 0 # Override class variable for Dog
def speak(self):
"""Override instance method."""
return f"{self.name} barks"
# Create instances
dog = Dog("Rex")
animal = Animal("Generic")
# Instance methods are overridden
print(animal.speak()) # "Generic makes a sound"
print(dog.speak()) # "Rex barks"
# Class methods use the class they're called on
print(Animal.species_total()) # "Total Animal: 1"
print(Dog.species_total()) # "Total Dog: 1"
# Static methods don't change
print(Dog.is_four_legged("dog")) # True
print(Dog.is_four_legged("bird")) # FalseExpected output:
Generic makes a sound
Rex barks
Total Animal: 1
Total Dog: 1
True
False
Each subclass can override instance methods and maintain its own class variables. Static methods stay the same across the hierarchy. Notice that Dog has its own species_count = 0, this shadows the parent class's species_count, so dog instances and animal instances track their counts independently. The species_total class method works correctly for both because it uses cls.__name__ and cls.species_count, adapting automatically to whichever class it's called on.
Real-World Patterns
Pattern 1: JSON Configuration with Alternate Constructors
import json
class Config:
def __init__(self, database, api_key, debug):
self.database = database
self.api_key = api_key
self.debug = debug
@classmethod
def from_json_file(cls, filepath):
"""Load config from JSON file."""
with open(filepath, "r") as f:
data = json.load(f)
return cls(
database=data["database"],
api_key=data["api_key"],
debug=data.get("debug", False)
)
@classmethod
def from_env(cls):
"""Load config from environment variables."""
import os
return cls(
database=os.getenv("DB_URL"),
api_key=os.getenv("API_KEY"),
debug=os.getenv("DEBUG", "false").lower() == "true"
)
def get_connection_string(self):
"""Instance method: build connection string."""
return f"postgresql://{self.database}"
# Use it
# config = Config.from_json_file("config.json")
# config = Config.from_env()
config = Config("localhost:5432", "secret123", True)
print(config.get_connection_string())Expected output:
postgresql://localhost:5432
This pattern is everywhere in production Python code. You'll see it in Django's model managers, SQLAlchemy's session factories, and configuration libraries. The from_json_file and from_env class methods give consumers of your API multiple clean ways to initialize the object without polluting __init__ with conditional logic and a dozen optional parameters.
Pattern 2: Singleton Pattern with Class Methods
class Logger:
_instance = None
def __init__(self):
self.logs = []
@classmethod
def get_instance(cls):
"""Return the single Logger instance."""
if cls._instance is None:
cls._instance = cls()
return cls._instance
def log(self, message):
"""Instance method: add a log entry."""
self.logs.append(message)
@staticmethod
def format_timestamp():
"""Static method: utility for timestamps."""
from datetime import datetime
return datetime.now().isoformat()
# Both calls return the *same* instance
logger1 = Logger.get_instance()
logger2 = Logger.get_instance()
logger1.log("First message")
logger2.log("Second message")
print(len(logger1.logs)) # 2 (both added to the same instance)
print(logger1 is logger2) # True (same object)Expected output:
2
True
The singleton pattern relies on the class method having access to cls._instance, a class-level variable that persists across all calls. The first call creates the instance and stores it; subsequent calls return the same object. The static method format_timestamp has no connection to any instance, it's just a utility that lives in the Logger namespace because it's conceptually related to logging. This example shows all three method types working together in service of a coherent design.
Pattern 3: Fluent API with Method Chaining
class QueryBuilder:
def __init__(self):
self.conditions = []
self.order_field = None
self.limit_count = None
def where(self, condition):
"""Add a WHERE clause."""
self.conditions.append(condition)
return self
def order_by(self, field):
"""Add ORDER BY clause."""
self.order_field = field
return self
def limit(self, count):
"""Add LIMIT clause."""
self.limit_count = count
return self
def build(self):
"""Build the final query string."""
query = "SELECT * FROM users"
if self.conditions:
query += " WHERE " + " AND ".join(self.conditions)
if self.order_field:
query += f" ORDER BY {self.order_field}"
if self.limit_count:
query += f" LIMIT {self.limit_count}"
return query
# Chain methods
query = (QueryBuilder()
.where("age > 18")
.where("city = 'NYC'")
.order_by("name")
.limit(10)
.build())
print(query)Expected output:
SELECT * FROM users WHERE age > 18 AND city = 'NYC' ORDER BY name LIMIT 10
Every method except build returns self, which is what makes the chain possible. This fluent interface pattern reads almost like English, it's expressive, self-documenting, and easy to extend. Adding a new clause type means adding a new method that appends to some list and returns self. Django's ORM uses exactly this pattern: User.objects.filter(age__gt=18).order_by('name').values('email')[:10].
Summary
Let's lock this down with a mental model that will stick. Every method in Python answers one question first: "What does this function implicitly receive?" Instance methods receive the object. Class methods receive the class. Static methods receive nothing.
-
Instance methods (
self) operate on individual objects and can access or modify instance state. They're your everyday method for object behavior. If the method body would useself.anything, it's an instance method. -
Class methods (
cls) operate on the class itself and are perfect for alternate constructors and factory patterns. The key advantage over standalone functions is thatclsautomatically becomes the subclass when called on a subclass, making your code extensible. Use them when you need to create instances from different data formats or manage class-level shared state. -
Static methods (no special parameter) are utility functions grouped under a class for organization. They don't access instance or class state, they're just functions that live in a namespace. Use them for validators, formatters, converters, and any pure computation that's logically related to a concept but doesn't need object or class data.
The key difference? What gets passed as the first parameter, and what that parameter gives you access to. self gives you the instance. cls gives you the class. Nothing gives you... nothing (but you're in a namespace). Master these three, and you'll write cleaner, more maintainable OOP code. You'll also understand how frameworks like Django and Flask build their robust APIs, because those frameworks use all three method types extensively, and now you can read their source code fluently rather than guessing at what each decorator does.
The investment you're making in these fundamentals pays dividends across the entire Python ecosystem. Every time you design a class going forward, you'll make a deliberate, informed choice about which method type serves each behavior best. That's what separates code that just works from code that communicates clearly.