Intermediate Python 📂 Class and Object · 2 of 10 45 min read

Python OOP — Instance Methods, Class Variables & Class Methods

Master the three tools that make Python classes actually powerful: instance methods that act on a single object, class variables that share state across every instance, and class methods that own the class itself — perfect for alternate constructors like from_csv and class-wide operations. A coffee-shop-chain analogy, three animated SVG diagrams, the shadow-attribute trap, the mutable-class-variable trap, and 10 golden rules.

Section 01

The Story That Explains These Three Tools

The Coffee Shop Chain — "Bean There"
You own Bean There, a coffee-shop chain with 200 stores worldwide. Each individual store has its own cash register, its own daily revenue, its own baristas — these are instance variables, unique per store.

But every store shares the same menu, the same prices, the same logo — these are class variables, one truth shared across the whole chain.

When a customer orders a latte at the Mumbai store, only that store's revenue moves. That's an instance method — behaviour that runs on one specific store. But when HQ raises the latte price from $4.50 to $5.00, it applies to every store instantly. That's a class method — one call, whole chain updates.
🌟
The Three Tools in One Sentence

Instance methods = behaviour of one object. Class variables = data shared by every object. Class methods = behaviour that acts on the class itself (or creates new objects). Master these three and you can express almost any real-world domain in Python.


Section 02

Where We Are — What You Already Know

You already know how to write a class with __init__ and instance variables. Something like this:

class CoffeeShop:
    def __init__(self, city, staff_count):
        self.city         = city         # instance variable — per store
        self.staff_count  = staff_count  # instance variable — per store
        self.daily_revenue = 0.0       # instance variable — per store

store1 = CoffeeShop("Mumbai", 8)
store2 = CoffeeShop("London", 12)

This gives you objects that hold data. What we need next is objects that can also do things (normal methods), classes that hold data shared by every instance (class variables), and methods that own the class itself (class methods). Let's take them one at a time.


Section 03

Normal Methods (Instance Methods) — Behaviour Attached to Each Object

A normal method — also called an instance method — is just a function defined inside a class whose first parameter is self. When you call it via obj.method(), Python transparently passes obj in as self, so the method knows which specific object it's working on.

🛠️ Anatomy of an Instance Method
Definition
Written inside the class body: def serve(self, item):
First param
self — receives the object the method was called on
Reads state
Uses self.x to read/write that object's own instance variables
Called
store1.serve("latte") — Python passes store1 as self
class CoffeeShop:
    def __init__(self, city):
        self.city          = city
        self.daily_revenue = 0.0
        self.orders        = []

    # ── Normal (instance) method — acts on THIS store ──
    def serve(self, item, price):
        self.daily_revenue += price
        self.orders.append(item)
        print(f"[{self.city}] Served {item} for ${price}")

    # ── Another instance method that reports state ──
    def summary(self):
        print(f"{self.city}: {len(self.orders)} orders, ${self.daily_revenue}")

# Create two independent stores
mumbai = CoffeeShop("Mumbai")
london = CoffeeShop("London")

mumbai.serve("latte", 4.5)         # self = mumbai
mumbai.serve("muffin", 3.5)        # self = mumbai
london.serve("espresso", 3.0)     # self = london

mumbai.summary()                    # uses Mumbai's data only
london.summary()                    # uses London's data only
OUTPUT
[Mumbai] Served latte for $4.5 [Mumbai] Served muffin for $3.5 [London] Served espresso for $3.0 Mumbai: 2 orders, $8.0 London: 1 orders, $3.0

Animated Diagram — An Instance Method Acts on One Specific Object

Watch mumbai.serve("latte", 4.5) flow: Python passes mumbai in as self, and the method mutates only that store's revenue. London and Tokyo aren't even aware anything happened.

INSTANCE METHOD  ·  SELF POINTS TO ONE OBJECT ONLY
mumbai.serve("latte", 4.5) Python passes mumbai as self mumbai city = 'Mumbai' daily_revenue $0.00 $4.50 self = mumbai london city = 'London' daily_revenue $0.00 unchanged tokyo city = 'Tokyo' daily_revenue $0.00 unchanged +$4.50

Only Mumbai's revenue moves. London and Tokyo are untouched because self pointed only to Mumbai.


Section 04

Class Variables — One Copy, Shared by Every Object

A class variable is defined at the class level, not inside __init__. There's exactly one copy and every instance can see it. Think of it as "facts that belong to the concept, not to any individual object."

class CoffeeShop:
    # ── Class variables — declared at the class level, ONE copy total ──
    company_name  = "Bean There"
    menu          = {"latte": 4.50, "espresso": 3.00, "muffin": 3.50}
    total_stores  = 0

    def __init__(self, city):
        # ── Instance variables — one copy per object ──
        self.city          = city
        self.daily_revenue = 0.0

        # Bump the shared counter when a new store opens
        CoffeeShop.total_stores += 1

mumbai = CoffeeShop("Mumbai")
london = CoffeeShop("London")
tokyo  = CoffeeShop("Tokyo")

# Access class variables through the class itself
print(CoffeeShop.company_name)     # Bean There
print(CoffeeShop.total_stores)     # 3
print(CoffeeShop.menu["latte"])    # 4.5

# You CAN also access them through any instance — they're shared
print(mumbai.company_name)         # Bean There
print(london.menu["latte"])        # 4.5 — same object as CoffeeShop.menu
🔑
Class Variable vs Instance Variable — The One-Line Test

Class variable — one copy total, shared. Written directly under the class line: menu = {...}. Instance variable — one copy per object. Written with self. inside a method: self.city = city. If you change your mind about which one you need, that's a design decision — not a syntax fix.

Animated Diagram — Class Variable Is Shared, Instance Variable Is Independent

Watch two events. First, HQ updates the shared menu (CoffeeShop.menu["latte"] = 5.00) — all three stores see the new price instantly because they share one menu. Then Mumbai serves a latte — only Mumbai's revenue moves because daily_revenue is per-store.

ONE SHARED MENU  ·  THREE INDEPENDENT REVENUES
CLASS VARIABLE  ·  ONE COPY, SHARED CoffeeShop.menu["latte"] $4.50 $5.00 mumbai reads menu: latte $4.50 latte $5.00 daily_revenue $0.00 $5.00 london reads menu: latte $4.50 latte $5.00 daily_revenue $0.00 tokyo reads menu: latte $4.50 latte $5.00 daily_revenue $0.00

One menu update ripples to every store. But daily_revenue lives per-store — Mumbai's rings up, London and Tokyo stay at zero.


Section 05

The Two Gotchas of Class Variables

Class variables are simple until you try to change them the wrong way. Two traps bite almost every Python beginner. Both come from Python's rule that reading uses class fallback, but writing creates an instance variable.

Gotcha #1 — Writing Through an Instance Creates a Shadow

class CoffeeShop:
    total_stores = 0              # class variable

s1 = CoffeeShop()
s2 = CoffeeShop()

# WRONG: this does NOT update the shared counter
s1.total_stores = 99              # creates a NEW instance variable on s1

print(s1.total_stores)         # 99   (shadow — s1's own attribute)
print(s2.total_stores)         # 0    (still reading class var)
print(CoffeeShop.total_stores) # 0    (class var untouched)

# RIGHT: update via the class itself
CoffeeShop.total_stores = 99
print(s2.total_stores)         # 99   (all instances see it now)
⚠️
The Rule

Writing instance.attr = value always creates an instance variable — even if a class variable with the same name exists. To modify a class variable, write it through the class: ClassName.attr = value.

Gotcha #2 — Mutable Class Variables Are Shared State

class CoffeeShop:
    orders = []                    # class variable — a list (mutable!)

    def __init__(self, city):
        self.city = city

s1 = CoffeeShop("Mumbai")
s2 = CoffeeShop("London")

s1.orders.append("latte")      # mutates the SHARED list in place!

print(s1.orders)                # ['latte']
print(s2.orders)                # ['latte']  ← ??!
print(s1.orders is s2.orders)   # True — same list object!
🔥
The Fix

Never put a mutable object (list, dict, set) as a class variable unless you truly want every instance to share and mutate the same one. For per-instance lists, create them in __init__: self.orders = []. Only use class variables for immutable shared data like strings, numbers, or tuples — or for genuinely global registries.


Section 06

Class Methods — Functions That Own the Class Itself

A class method is a method whose first parameter is the class itself (conventionally named cls), not an instance. You mark it with the @classmethod decorator. You call it on the class, not on an object.

Two overwhelmingly common use cases:

🏗️
Alternate Constructor
factory pattern
A second way to create an instance. Example: CoffeeShop.from_csv("Mumbai,8") parses a string and returns a new store. Keeps the parsing logic tied to the class.
📩
Class-Wide Operation
affects everyone
An operation that changes shared state, not any single object. CoffeeShop.update_price("latte", 5.00) raises the price for the whole chain.
📊
Aggregate Query
no instance needed
Answers questions about the class itself, not any one object. CoffeeShop.how_many_open() reports the total count without needing a specific store.
class CoffeeShop:
    company_name = "Bean There"
    menu         = {"latte": 4.50, "espresso": 3.00}
    total_stores = 0

    def __init__(self, city, staff_count):
        self.city         = city
        self.staff_count  = staff_count
        self.daily_revenue = 0.0
        CoffeeShop.total_stores += 1

    # ── Class method as ALTERNATE CONSTRUCTOR ──
    @classmethod
    def from_csv(cls, row: str):
        """Build a CoffeeShop from a CSV row like 'Mumbai,8'."""
        city, staff = row.split(",")
        return cls(city, int(staff))       # cls() == CoffeeShop()

    # ── Class method for CLASS-WIDE OPERATION ──
    @classmethod
    def update_price(cls, item, new_price):
        """HQ raises a price everywhere at once."""
        cls.menu[item] = new_price
        print(f"HQ: {item} is now ${new_price} at every store")

    # ── Class method for AGGREGATE QUERY ──
    @classmethod
    def how_many_open(cls):
        return cls.total_stores

# ── Regular constructor ──
mumbai = CoffeeShop("Mumbai", 8)

# ── Alternate constructor (class method) ──
london = CoffeeShop.from_csv("London,12")
tokyo  = CoffeeShop.from_csv("Tokyo,20")

# ── Class-wide operation ──
CoffeeShop.update_price("latte", 5.00)

# ── Aggregate query ──
print(CoffeeShop.how_many_open())  # 3
print(mumbai.menu["latte"])          # 5.0 (Mumbai sees the update)
OUTPUT
HQ: latte is now $5.0 at every store 3 5.0

Animated Diagram — self vs cls

The difference in one picture. An instance method receives self — a specific existing object. A class method receives cls — the class itself, which it uses to read shared state or build brand-new objects.

SELF POINTS TO ONE OBJECT  ·  CLS POINTS TO THE CLASS
INSTANCE METHOD mumbai.serve("latte") self EXISTING OBJECT (mumbai) city = 'Mumbai' revenue = $0.00 revenue = $4.50 acts on this one object no new object is created CLASS METHOD CoffeeShop.from_csv("Rome,7") cls THE CLASS ITSELF class CoffeeShop NEW OBJECT city='Rome', staff=7 cls() creates a fresh instance

self = "which object am I working on?"  ·  cls = "which class am I working on?"


Section 07

Why cls and Not Just CoffeeShop?

Inside a class method you could technically hard-code the class name — but that breaks inheritance. cls always refers to the actual class the method was called on, which is the point.

class CoffeeShop:
    company_name = "Bean There"

    @classmethod
    def from_csv(cls, row):
        city, staff = row.split(",")
        return cls(city, int(staff))    # ← uses cls, not CoffeeShop

class PremiumCoffeeShop(CoffeeShop):     # subclass
    company_name = "Bean There Premium"

# Because from_csv uses cls, this returns a PremiumCoffeeShop — not a CoffeeShop
p = PremiumCoffeeShop.from_csv("Milan,10")
print(type(p).__name__)             # PremiumCoffeeShop
print(p.company_name)              # Bean There Premium
💡
The Habit

Inside a @classmethod, always use cls — never the hard-coded class name. It costs nothing to write and makes your class play nicely with future subclasses.


Section 08

Instance Method vs Class Method — At a Glance

QuestionInstance MethodClass Method
Decorator required? No @classmethod
First parameter self — the object cls — the class
Called via obj.method() Class.method() (or obj.method())
Needs an existing object? Yes No
Can read/write instance vars? Yes — via self.x No — no self
Can read/write class vars? Read yes; write only via class name Yes — via cls.x
Can create new instances? Rarely (usually not the point) Yes — return cls(...)
Typical use case Behaviour on this one object Alternate constructor, class-wide operation, aggregate

Section 09

Complete Practical Example — All Three Together

One class that uses instance methods, class variables, and class methods in a realistic small-domain example. Ready to paste, run, and read line by line.

class CoffeeShop:
    # ══ CLASS VARIABLES — shared across every store ══
    company_name = "Bean There"
    menu         = {"latte": 4.50, "espresso": 3.00, "muffin": 3.50}
    tax_rate     = 0.10
    total_stores = 0

    def __init__(self, city, staff_count):
        # ══ INSTANCE VARIABLES — one copy per store ══
        self.city          = city
        self.staff_count   = staff_count
        self.daily_revenue = 0.0
        self.orders        = []               # fresh list per instance
        CoffeeShop.total_stores += 1

    # ══ INSTANCE METHOD — acts on THIS store ══
    def serve(self, item):
        if item not in CoffeeShop.menu:
            raise ValueError(f"{item} not on menu")
        price = CoffeeShop.menu[item] * (1 + CoffeeShop.tax_rate)
        self.daily_revenue += price
        self.orders.append(item)
        print(f"[{self.city}] Served {item} for ${price:.2f}")

    # ══ INSTANCE METHOD — reports THIS store's state ══
    def report(self):
        print(f"{self.city:8s}  orders={len(self.orders):2d}  "
              f"revenue=${self.daily_revenue:7.2f}")

    # ══ CLASS METHOD — alternate constructor ══
    @classmethod
    def from_csv(cls, row: str):
        city, staff = row.split(",")
        return cls(city.strip(), int(staff))

    # ══ CLASS METHOD — HQ changes shared state ══
    @classmethod
    def update_price(cls, item, new_price):
        cls.menu[item] = new_price
        print(f"HQ: {item} is now ${new_price:.2f} everywhere")

    # ══ CLASS METHOD — aggregate query ══
    @classmethod
    def chain_stats(cls):
        return f"{cls.company_name}: {cls.total_stores} stores open"


# ── Open three stores — two ways ─────────────────────────────
mumbai = CoffeeShop("Mumbai", 8)                # normal constructor
london = CoffeeShop.from_csv("London, 12")      # class-method constructor
tokyo  = CoffeeShop.from_csv("Tokyo, 20")

# ── Serve some drinks (instance methods on each store) ───────
mumbai.serve("latte")
mumbai.serve("muffin")
london.serve("espresso")
tokyo.serve("latte")

# ── HQ raises the latte price (class method → class variable) ─
CoffeeShop.update_price("latte", 5.00)
tokyo.serve("latte")    # now costs $5.00 + tax

print()
for store in (mumbai, london, tokyo):
    store.report()

print()
print(CoffeeShop.chain_stats())
OUTPUT
[Mumbai] Served latte for $4.95 [Mumbai] Served muffin for $3.85 [London] Served espresso for $3.30 [Tokyo] Served latte for $4.95 HQ: latte is now $5.00 everywhere [Tokyo] Served latte for $5.50 Mumbai orders= 2 revenue=$ 8.80 London orders= 1 revenue=$ 3.30 Tokyo orders= 2 revenue=$ 10.45 Bean There: 3 stores open
🏆
Notice What Just Happened

Every store served its own orders and tracked its own revenue independently (instance methods, instance variables). They all read from one shared menu and one shared tax rate (class variables). HQ raised the latte price with a single call (class method modifying a class variable), and both alternate creation via from_csv and aggregate reporting via chain_stats ran on the class itself — no specific store needed.


Section 10

When to Use Which — Decision Grid

👤
Use Instance Method When...
The behaviour needs data specific to one object. "This account's balance", "this store's revenue", "this student's marks". 99% of the methods you write will be instance methods.
first parameter: self
📚
Use Class Variable When...
The value is the same for every instance — configuration constants, shared reference data, a counter of "how many exist." Prefer immutable types (str, int, tuple, frozenset) to avoid the shared-mutable trap.
one copy, shared
🏗️
Use Class Method as Alternate Constructor
You want more than one way to build an instance — from_csv, from_dict, from_json, today(). Keeps the parsing/validation right where the class lives.
factory pattern
📡
Use Class Method for Class-Wide Ops
You want to change shared state that affects every instance — updating a shared price, toggling a global feature flag, refreshing shared cache. Signals "this is a policy change, not one object's business."
cls.something = new
Don't Use Class Method as Regular Method
If your method reads or writes instance state, it should be an instance method. Adding @classmethod just because it "feels neater" removes access to self.
need self? not classmethod
Don't Assign Class Var Via Instance
self.total_stores = 99 creates a shadow, not an update. To modify a class variable, write it through the class: ClassName.total_stores = 99 — or via cls.total_stores inside a class method.
shadow attribute trap

Section 11

Common Mistakes (and Fixes)

MistakeWhat Goes WrongFix
Forgetting self in a method TypeError: takes 0 positional arguments but 1 was given Add self as the first parameter
self.total = 99 to update class var Creates instance shadow — other instances unchanged ClassName.total = 99
Mutable class variable (orders = []) Every instance shares — one .append() hits all Create self.orders = [] in __init__
Forgetting @classmethod Python treats it as instance method — first arg is self Add @classmethod decorator on the line above
Hard-coding class name inside class method Subclasses build the wrong type Use cls — that's why it's there
Calling self.method() forgetting the () Returns the bound method object, not its result Add the parentheses: self.method()
@classmethod that never touches cls You probably wanted @staticmethod Either use cls, or switch to @staticmethod

Section 12

Quick Reference

TaskSyntaxNotes
Define instance methoddef serve(self, item):First arg always self
Call instance methodstore.serve("latte")Python passes store as self
Read instance variable insideself.cityUses this object's memory
Declare class variablemenu = {...} under class lineOne copy, shared by every instance
Read class variableClassName.menu or self.menuBoth work for reading
Update class variableClassName.menu["x"] = ...Never self.menu = ...
Define class method@classmethod + def m(cls, ...)Decorator above def
Alternate constructorreturn cls(...)Use cls, not ClassName
Call class methodClassName.from_csv("...")No instance needed
Count instancesIncrement class var in __init__ClassName.total += 1

Section 13

Golden Rules

🏆 Methods, Class Variables & Class Methods — Non-Negotiable Rules
1
Every method's first parameter tells you what it operates on. self = one specific object. cls = the class itself. There's no third option in this tutorial.
2
99% of your methods should be instance methods. Only reach for @classmethod when you don't need self — usually an alternate constructor, a class-wide operation, or an aggregate query.
3
Class variables are for values every instance genuinely shares — configuration constants, shared reference data, or a counter. If each instance needs its own copy, it belongs in __init__ as an instance variable.
4
Never put a mutable object as a class variable unless the whole point is shared, mutable state. Lists, dicts, and sets should live in __init__ as self.x = [] so each object gets its own copy.
5
To update a class variable, go through the class itself: CoffeeShop.total_stores += 1, never self.total_stores += 1. The instance form silently creates a shadow attribute and leaves the class variable untouched.
6
Always put @classmethod on the line above def, and name the first parameter cls. Skipping either one breaks the method in a confusing way that Python won't clearly diagnose.
7
Inside a class method, use cls(...) to build new objects, not ClassName(...). That single choice preserves correct behaviour under inheritance — free, at no code cost.
8
Prefer alternate constructors (Employee.from_csv, Date.today) over "helper functions" outside the class. They keep parsing and building logic tied to the type they produce.
9
Reading a class variable via an instance (store.company_name) is fine — Python falls back to the class automatically. But the moment you write store.company_name = "...", you've made a per-instance attribute. Read anywhere; write only via the class.
10
When in doubt, ask two questions. "Does this need to know about one specific object?" → instance method. "Is this data the same for every object?" → class variable. Everything else is decoration.