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

Instance methoddef printdesc(self):self → the one objectcalled asjames.printdesc()reads/changes thatobject's own datathe default kind ofmethod — no decoratorClass method@classmethoddef change_comp(cls, ...):cls → the class itselfcalled asPerson.change_comp(...)changes data sharedby every objectaffects James and Sarahat the same timeStatic method@staticmethoddef printstring(s):no self, no clscalled asPerson.printstring(s)plain function that justlives in the class namespaceno access to object orclass state at all

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.