Object-Oriented Programming
Instance, Class, and Static Methods (and Dunder Attributes)
7 min read
Three flavors of method
Every method you define inside a class falls into one of three categories, depending on what it automatically receives as its first argument: the specific object (self), the class itself (cls), or nothing at all.
Diagram — Instance, Class, and Static Methods Compared
You already know the first kind from Classes and Objects — this lesson covers the other two, plus a handful of special "dunder" (double-underscore) methods that let your objects hook into Python's built-in behavior.
Instance methods (recap)
An instance method takes self as its first parameter and operates on one specific object's data:
class Person():
Company = 'Google'
def __init__(self, designation, address):
self.designation = designation
self.address = address
def printdesc(self):
print(self.designation)
print(self.address)
print(self.Company)
This is the default — a method with no decorator is an instance method. The other two kinds are marked explicitly with a decorator, a @name line placed directly above the def.
Class methods: shared data, changed everywhere at once
A class method is marked with @classmethod and takes cls — a reference to the class itself — instead of self. Because it operates on the class, not on one object, changes it makes are visible to every instance immediately:
class Person():
Company = 'Google'
def __init__(self, designation, address):
self.designation = designation
self.address = address
def printdesc(self):
print(self.designation)
print(self.address)
print(self.Company)
@classmethod
def change_comp(cls, new_company):
cls.Company = new_company
James = Person('Software Engineer', 'Seattle')
Sarah = Person('Manager', 'Austin')
Sarah.printdesc()
Sarah.change_comp('Apple')
Sarah.printdesc()
James.printdesc()
Output:
Manager
Austin
Google
Manager
Austin
Apple
Software Engineer
Seattle
Apple
Calling Sarah.change_comp('Apple') updated Company for both Sarah and James — because cls.Company = new_company assigns to the class variable itself, not to an instance. This is exactly the shadowing distinction from the previous lesson, from the other direction: assigning through self (or through an instance directly) creates a private instance variable, while assigning through cls changes the one copy every object shares. Reach for a class method whenever the operation is conceptually about the type — like "change the company for everyone" — rather than about one particular object.
Static methods: a function that just lives in the class
A static method, marked with @staticmethod, receives neither self nor cls. It behaves like an ordinary function — it can't read or change instance or class data — but it's grouped inside the class because it's conceptually related to it:
class Person():
Company = 'Google'
def __init__(self, designation, address):
self.designation = designation
self.address = address
@staticmethod
def printstring(string):
print(string)
James = Person('Software Engineer', 'Seattle')
Sarah = Person('Manager', 'Austin')
Sarah.printstring("My name is Sarah")
James.printstring("My name is James")
Person.printstring("My name is Person")
Output:
My name is Sarah
My name is James
My name is Person
Notice printstring can be called through an instance (Sarah.printstring(...)) or directly through the class (Person.printstring(...)) — it doesn't matter which, since it never looks at self or cls in the first place. Use a static method for a utility function that's logically related to a class — like a validator or a formatter — but doesn't need any of that class's data to do its job.
Built-in class attributes
Beyond whatever attributes you define yourself, every class and object carries a handful of attributes Python sets up automatically:
class Person():
company = "apple"
def __init__(self, name, empid):
self.name = name
self.empid = empid
def display(self):
print("name", self.name)
print("empid", self.empid)
person1 = Person('ashish', 122)
print(Person.__dict__)
print(Person.__name__)
print(Person.__module__)
Output:
{'__module__': '__main__', 'company': 'apple', '__init__': <function Person.__init__ at ...>, 'display': <function Person.display at ...>, '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}
__name__: Person
__module__: __main__
__dict__— a dictionary of everything defined directly on the class (or, on an instance, everything defined on that instance)__name__— the class's name as a string__module__— the name of the module the class was defined in
You'll rarely need these day to day, but they're useful for debugging and are exactly what tools like debuggers and serialization libraries rely on under the hood.
__str__: controlling how an object prints
By default, printing an object gives you the same unhelpful memory address you saw back in Classes and Objects — <__main__.Person object at 0x...>. Defining a __str__ method lets your class provide something more useful. Python calls it automatically whenever the object is passed to print() or str():
class Person():
company = "apple"
def __init__(self, name, empid):
self.name = name
self.empid = empid
def __str__(self):
return "Helloworld"
person1 = Person('ashish', 122)
print(person1)
Output:
Helloworld
A real __str__ would normally build a message from the object's own data — something like return f"{self.name} (ID {self.empid})" — rather than a fixed string. The one requirement is that it must return a string; whatever comes back is exactly what print() displays. (If you've since seen __repr__ elsewhere: it serves a similar purpose but is meant for an unambiguous, developer-facing representation — often used in a debugger or the interactive shell — while __str__ is for the readable, user-facing version. Defining __str__ is usually enough for everyday classes.)
__del__: the destructor
__del__ is the mirror image of __init__ — instead of running when an object is created, it runs when the object is about to be destroyed:
class Person():
def __del__(self):
print("Person object destroyed")
Python calls object.__del__(self) automatically as part of cleaning up an object that's no longer referenced anywhere. In practice, __del__ is far less commonly used than __init__, for a good reason worth calling out explicitly: you don't control exactly when it runs. Python's garbage collector decides when an object with no remaining references actually gets cleaned up, and by the time __del__ runs, other global objects it might depend on could already be gone. For resources that need reliable, predictable cleanup — closing a file, releasing a network connection — the pattern you already know from Modules, Libraries, and File I/O (a with block, or try/finally from Exception Handling) is the standard, safer tool. __del__ is worth recognizing when you see it, but reach for it rarely.
Key takeaway
Instance methods (plain self) operate on one object; class methods (@classmethod, cls) operate on the class and affect every instance at once; static methods (@staticmethod, no automatic argument) are just functions grouped inside a class for organization. Built-in attributes like __dict__, __name__, and __module__ expose what Python already tracks about a class. Dunder methods let your objects hook into built-in behavior — __str__ controls how an object prints, and __del__ runs when it's destroyed, though explicit cleanup with try/finally or with is usually the more reliable choice.
What's next?
You've now covered the full toolkit this course set out to teach — from variables and control flow through functions, comprehensions, exception handling, functional tools like map() and lambda, and now classes and objects. The best next step is to combine them: try modeling something you know well (a library, a game, a to-do list) as a class with its own instance data and methods, using a try/except block anywhere user input could go wrong. That's the same shape as most real Python programs, just built from pieces you now have all in hand.