Intermediate Python 📂 Class and Object · 4 of 10 37 min read

Python OOP — Method Overriding, Static Methods & Static Variables

Master the three tools that make Python class hierarchies flexible and reusable: static variables (class-level constants shared by every instance and subclass), static methods (@staticmethod pure utility functions), and method overriding (subclasses replacing parent behaviour to enable polymorphism). Learn through a coffee-shop-with-three-signature-drinks analogy, three animated SVG diagrams, and a full Espresso/Latte/Cappuccino example that uses all three together.

Section 01

The Story That Ties These Three Ideas Together

The Coffee Shop With Three Signature Drinks
Step into any branch of a coffee shop chain. Three things live in there together:

On the wall: posted rules — STANDARD_CUP = 250 ml, MAX_BREW_TEMP = 95°C, TAX_RATE = 10%. Same everywhere. Nobody edits them. These are static variables.

Behind the counter: a thermometer, a scale, a stopwatch, a calculator. Anyone can grab one. They don't care whose drink is being made. Pure tools. These are static methods.

At the espresso machine: three signature drinks — Espresso, Latte, Cappuccino. Every barista knows a generic prepare() step, but each drink is prepared completely differently. The word "prepare" doesn't mean "grind and extract 25 seconds" for a Latte — it means "steam milk to 65°C and pour." Same call, different result. That's method overriding.

Wire all three together and you get a class hierarchy that shares what should be shared and customises what should be customised.
🌟
The Three Tools in One Sentence

Static variables = one copy of a constant, shared by every instance. Static methods = pure functions living inside the class namespace. Method overriding = a child class redefining a parent's method to change its behaviour. Together they turn a rigid class into a flexible family of classes.


Section 02

Meet All Three at a Glance

📌
Static Variables
class-level constants
Defined at the class level. One copy total. Shared by every instance and every subclass. Named UPPER_SNAKE_CASE to signal "don't reassign." Example: Drink.TAX_RATE = 0.10.
🔧
Static Methods
@staticmethod  ·  no self, no cls
Plain functions inside the class namespace. Take no self, no cls — only their own arguments. Called on the class: Drink.celsius_to_fahrenheit(65). Perfect for validators, converters, and pure calculations.
🔁
Method Overriding
same name, new behaviour
A subclass defines a method with the same name as one in its parent. When called on the subclass, Python uses the new version. Same interface, different implementation — this is polymorphism.

Section 03

Static Variables — The Shop's Posted Rules

A static variable is a value defined directly under the class line, not inside __init__. There is exactly one copy in memory and every instance reads from it.

class Drink:
    # ══ STATIC VARIABLES — one copy, shared by every drink ══
    STANDARD_CUP_ML  = 250
    MAX_BREW_TEMP_C  = 95
    TAX_RATE         = 0.10
    CURRENCY         = "$"

    def __init__(self, base_price):
        # INSTANCE variables — one per drink
        self.base_price = base_price

# Read via the class (canonical form)
print(Drink.STANDARD_CUP_ML)   # 250
print(Drink.TAX_RATE)          # 0.1
print(Drink.CURRENCY)          # $

# Every instance sees the same value — it's not copied per drink
d1 = Drink(3); d2 = Drink(5); d3 = Drink(4)
print(d1.TAX_RATE, d2.TAX_RATE, d3.TAX_RATE)     # 0.1 0.1 0.1

Animated Diagram — One Posted Rulebook, Every Drink Reads It

STATIC VARIABLES  ·  ONE COPY, EVERY INSTANCE READS
STATIC VARIABLES  ·  class Drink STANDARD_CUP_ML = 250 MAX_BREW_TEMP_C = 95 TAX_RATE         = 0.10 espresso base_price = 3.00 reads TAX_RATE = 0.10 latte base_price = 5.00 reads TAX_RATE = 0.10 cappuccino base_price = 4.50 reads TAX_RATE = 0.10

Three drinks, three prices — but only one TAX_RATE. Change the constant on the class and every drink instantly sees the new value.

⚠️
Never Reassign a Static Constant via an Instance

latte.TAX_RATE = 0.15 does not update the constant. It creates a per-instance shadow that only latte sees — every other drink still reads 0.10. To modify a static value (rarely a good idea), do it through the class: Drink.TAX_RATE = 0.15.


Section 04

Static Methods — The Shared Utility Shelf

A static method is defined with the @staticmethod decorator and takes no self and no cls. It's a plain function that happens to live inside the class namespace. Use it for validators, converters, formatters, and pure calculations.

class Drink:
    STANDARD_CUP_ML = 250
    TAX_RATE        = 0.10

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

    # ══ STATIC METHOD — pure input → output, no self, no cls ══
    @staticmethod
    def celsius_to_fahrenheit(c):
        return c * 9 / 5 + 32

    @staticmethod
    def is_valid_size(size_ml):
        return 25 <= size_ml <= 500

    @staticmethod
    def calculate_total(price):
        # Uses the class-level constant — but no self/cls needed
        return price * (1 + Drink.TAX_RATE)


# Call BEFORE any drink exists — that's the whole point
print(Drink.celsius_to_fahrenheit(65))   # 149.0
print(Drink.is_valid_size(200))           # True
print(Drink.is_valid_size(1000))          # False
print(Drink.calculate_total(5.00))       # 5.5 (with 10% tax)

Animated Diagram — Static Method as a Pure Function

STATIC METHOD  ·  INPUT → PURE FUNCTION → OUTPUT
Drink.calculate_total(5.00) INPUT 5.00 STATIC METHOD @staticmethod calculate_total(price) return price * (1 + TAX_RATE) no self  ·  no cls  ·  pure OUTPUT 5.50 No instance is created  ·  no drink is affected  ·  just: input in, answer out.

A static method is a plain function that lives in the class namespace. Call it directly on the class — no instance needed.


Section 05

Method Overriding — Same Call, Different Behaviour

Method overriding is the moment a child class defines a method with the same name as one in its parent. When the method is called on the child, Python uses the child's version — the parent's version is silently ignored. This is what makes polymorphism possible: many types respond to the same command in different ways.

class Drink:
    def __init__(self, size_ml, base_price):
        self.size_ml   = size_ml
        self.base_price = base_price

    # A generic prepare() — subclasses will override this
    def prepare(self):
        print("Preparing a generic drink — hot water in a cup.")


class Espresso(Drink):
    # ── OVERRIDE — same name, brand-new implementation ──
    def prepare(self):
        print(f"Espresso ({self.size_ml}ml): grind beans, tamp, extract 25s.")


class Latte(Drink):
    def prepare(self):
        print(f"Latte ({self.size_ml}ml): pull espresso, steam milk to 65°C, pour art.")


class Cappuccino(Drink):
    def prepare(self):
        print(f"Cappuccino ({self.size_ml}ml): equal parts espresso, milk, foam.")


# Same call, different result — that's polymorphism
for drink in [Espresso(30, 3), Latte(250, 5), Cappuccino(180, 4.5)]:
    drink.prepare()
OUTPUT
Espresso (30ml): grind beans, tamp, extract 25s. Latte (250ml): pull espresso, steam milk to 65°C, pour art. Cappuccino (180ml): equal parts espresso, milk, foam.
🔑
The Rule Python Uses

When you call obj.method(), Python starts searching at the object's own class. The first match wins. If Espresso defines prepare(), Python uses that and never even looks at Drink.prepare(). This is the Method Resolution Order (MRO) — deterministic, cheap, and the foundation of polymorphism.

Animated Diagram — One Call, Three Different Outcomes

A single loop calls drink.prepare() on each of the three drinks. Watch how the same method name lights up a different code path in each subclass — and each produces its own distinct output.

METHOD OVERRIDING  ·  SAME CALL, DIFFERENT BEHAVIOUR
for drink in [...]: drink.prepare() Same call — Python dispatches to each drink's own prepare() PARENT  ·  Drink prepare()  ·  generic (never called) ESPRESSO (Drink) def prepare(self): grind → tamp → extract * OVERRIDE LATTE (Drink) def prepare(self): espresso + steamed milk * OVERRIDE CAPPUCCINO (Drink) def prepare(self): espresso + milk + foam * OVERRIDE OUTPUT Espresso: grind, tamp... OUTPUT Latte: pull, steam... OUTPUT Cappuccino: equal parts...

One line, three behaviours. Python picks the correct prepare() for each drink automatically — the parent's generic version never runs.


Section 06

Override + Extend — Using super() to Keep Parent Behaviour

Sometimes you don't want to replace the parent's method — you want to extend it. Call super().method() from within your override to run the parent's version first, then add your own logic.

class Drink:
    def __init__(self, size_ml, base_price):
        self.size_ml    = size_ml
        self.base_price = base_price

    def prepare(self):
        print("  1. Warm the cup")
        print("  2. Fill with liquid")


class FlavouredLatte(Drink):
    def __init__(self, size_ml, base_price, syrup):
        super().__init__(size_ml, base_price)
        self.syrup = syrup

    def prepare(self):
        super().prepare()                       # run the parent's steps first
        print(f"  3. Add {self.syrup} syrup")    # then extend
        print("  4. Steam milk and pour")


drink = FlavouredLatte(250, 5.50, syrup="vanilla")
drink.prepare()
OUTPUT
1. Warm the cup 2. Fill with liquid 3. Add vanilla syrup 4. Steam milk and pour
🏆
The Two Flavours of Overriding

Replace — override without calling super(). The parent's version never runs. Use when the child's behaviour is completely different. Extend — override and call super().method() inside. The parent runs, then your addition runs. Use when the child wants "parent's logic + a bit more."


Section 07

Putting All Three Together — The Complete Coffee Shop

A single hierarchy that uses static variables (shop rules), static methods (utility functions), and method overriding (each drink's unique preparation). This is the real payoff.

class Drink:
    # ══ STATIC VARIABLES — shared across every drink and every subclass ══
    STANDARD_CUP_ML  = 250
    MAX_BREW_TEMP_C  = 95
    TAX_RATE         = 0.10
    CURRENCY         = "$"

    def __init__(self, size_ml, base_price):
        if not Drink.is_valid_size(size_ml):     # uses a STATIC METHOD
            raise ValueError(f"{size_ml}ml is out of range")
        self.size_ml    = size_ml
        self.base_price = base_price

    # ── Generic prepare — subclasses OVERRIDE this ──
    def prepare(self):
        print("Generic drink: hot water, done.")

    # ── Instance method — uses static variable for its calculation ──
    def price_receipt(self):
        total = Drink.calculate_total(self.base_price)
        return f"  Price: {Drink.CURRENCY}{self.base_price:.2f} + tax → {Drink.CURRENCY}{total:.2f}"

    # ══ STATIC METHODS — utility functions grouped with the class ══
    @staticmethod
    def is_valid_size(size_ml):
        return 25 <= size_ml <= 500

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

    @staticmethod
    def calculate_total(price):
        return price * (1 + Drink.TAX_RATE)


class Espresso(Drink):
    def __init__(self):
        super().__init__(size_ml=30, base_price=3.00)

    # OVERRIDE — Espresso has its own steps
    def prepare(self):
        print(f"[Espresso {self.size_ml}ml] grind → tamp → extract for 25s")


class Latte(Drink):
    def __init__(self):
        super().__init__(size_ml=250, base_price=5.00)

    def prepare(self):
        temp_f = Drink.celsius_to_fahrenheit(65)     # uses a STATIC METHOD
        print(f"[Latte {self.size_ml}ml] pull espresso → steam milk to 65°C ({temp_f}°F) → pour art")


class Cappuccino(Drink):
    def __init__(self):
        super().__init__(size_ml=180, base_price=4.50)

    def prepare(self):
        print(f"[Cappuccino {self.size_ml}ml] equal parts espresso + steamed milk + foam")


class FlavouredLatte(Latte):
    def __init__(self, syrup):
        super().__init__()
        self.syrup = syrup
        self.base_price += 0.75              # flavour surcharge

    # OVERRIDE + EXTEND — run parent's Latte prepare, then add syrup step
    def prepare(self):
        super().prepare()
        print(f"              → finish with {self.syrup} syrup")


# ── Use the static method BEFORE any drink exists ────────────
print("Valid 150ml cup?", Drink.is_valid_size(150))
print("Valid 900ml cup?", Drink.is_valid_size(900))
print("Brew temp in F:", Drink.celsius_to_fahrenheit(Drink.MAX_BREW_TEMP_C))
print()

# ── Same loop, polymorphic dispatch on prepare() ─────────────
menu = [Espresso(), Latte(), Cappuccino(), FlavouredLatte(syrup="vanilla")]
for drink in menu:
    drink.prepare()                    # dispatched to the right override
    print(drink.price_receipt())         # inherited from Drink, uses static var + method
    print()
OUTPUT
Valid 150ml cup? True Valid 900ml cup? False Brew temp in F: 203.0 [Espresso 30ml] grind → tamp → extract for 25s Price: $3.00 + tax → $3.30 [Latte 250ml] pull espresso → steam milk to 65°C (149.0°F) → pour art Price: $5.00 + tax → $5.50 [Cappuccino 180ml] equal parts espresso + steamed milk + foam Price: $4.50 + tax → $4.95 [Latte 250ml] pull espresso → steam milk to 65°C (149.0°F) → pour art → finish with vanilla syrup Price: $5.75 + tax → $6.33
🏆
Everything Fits Together

Static variables (TAX_RATE, CURRENCY, MAX_BREW_TEMP_C) provide the one-truth constants. Static methods (is_valid_size, celsius_to_fahrenheit, calculate_total) are pure utilities every drink can call. Method overriding gives each drink its own prepare(). And FlavouredLatte demonstrates the "override + extend" pattern via super().prepare().


Section 08

When to Reach for Each Tool — Decision Grid

📌
Use a Static Variable When...
The value is the same for every instance and every subclass — configuration, business rules, physical constants. Prefer immutable types (str, int, tuple, frozenset).
UPPER_SNAKE_CASE constants
🔧
Use a Static Method When...
The function needs neither self nor cls, but still belongs conceptually to the class — validators, converters, formatters, pure calculations. Grouping keeps them discoverable.
is_valid_x, x_to_y, calc_z
🔁
Use Method Overriding When...
Different subclasses need to respond to the same command in different ways. This is polymorphism — write the loop once and each object does the right thing for its type.
child redefines parent method
🔁
Override + Extend When...
You want the parent's behaviour and a little extra. Call super().method(...) inside the override to run the parent first, then add your own steps.
super().method() first
Don't Use a Mutable Static Variable
A class-level list or dict is shared by every instance. One .append() hits them all. Put mutable per-instance data in __init__ instead.
shared-mutable trap
Don't Override Just to "Fix" the Parent
If every subclass overrides the same method the same broken way, the fix belongs in the parent. Overriding is for legitimate per-subclass differences, not workarounds.
fix at the right layer

Section 09

Common Mistakes (and Fixes)

MistakeWhat Goes WrongFix
Adding self to a static method You lose the "no instance needed" property; callers must build an object first Drop self. Add the @staticmethod decorator on the line above
Forgetting @staticmethod Python treats it as an instance method; first argument silently becomes self Add @staticmethod — decorator required
instance.CONSTANT = value Creates a per-instance shadow — the class constant stays unchanged Never write it. If you must change it, do it through the class: Drink.TAX_RATE = ...
Using a mutable static variable (orders = []) Every instance shares one list; a mutation from any of them leaks to all Put it in __init__: self.orders = []
Overriding without matching the parent's signature Callers that used the parent's signature break silently on child instances Keep the same parameters. To add new ones, give them defaults, or take *args, **kwargs
Overriding then forgetting super() when you meant to extend Parent's setup logic is silently skipped — hard-to-trace bugs Decide consciously: replace (no super) vs extend (call super)
Overriding when a config flag would do Class hierarchy explodes with one-line subclasses (class RedButton(Button)) Use a parameter or class variable in the parent instead of subclassing

Section 10

Quick Reference

TaskSyntaxNotes
Declare a static variableTAX_RATE = 0.10 under class lineUPPER_SNAKE_CASE, immutable by convention
Read a static variableDrink.TAX_RATECanonical form
Update a static variableDrink.TAX_RATE = 0.12Never via an instance
Declare a static method@staticmethod + def fn(x):Decorator on the line above def
Call a static methodDrink.is_valid_size(200)No instance needed
Override a methodRedefine with the same name in the childChild's version wins over parent's
Extend a methodOverride + call super().method(...)Parent runs, then your addition
Polymorphic loopfor x in items: x.prepare()Each object dispatches to its own override
Inspect lookup orderEspresso.mro()First class in the list that defines the method wins

Section 11

Golden Rules

☕ Overriding, Static Methods & Static Variables — Non-Negotiable Rules
1
A static variable is a class-level constant. One copy, shared by every instance and every subclass. Name it UPPER_SNAKE_CASE to signal "don't reassign."
2
Only put immutable values in static variables (numbers, strings, tuples, frozensets). A mutable class-level list or dict is shared by every instance — a bug factory.
3
Read static variables via the class: Drink.TAX_RATE. Writing them via an instance (d.TAX_RATE = 0.15) does not update the constant — it creates a silent per-instance shadow.
4
A static method takes neither self nor cls. If your method needs instance state, it's an instance method. If it needs the class, it's a class method. Only when it needs neither is it truly static.
5
Always put @staticmethod on the line above def. Forgetting the decorator makes Python treat it as an instance method — with silently wrong behaviour on the first call.
6
Reach for @staticmethod for validators, converters, formatters, and pure calculations that logically live with the class. If it has nothing to do with the class, leave it as a module function.
7
Method overriding is child-redefines-parent by using the same method name. The child's version wins — that's the whole rule. Python starts searching at the object's class and walks up.
8
Decide consciously between replacing and extending. Replacing = don't call super(). Extending = call super().method(...) inside your override so the parent's logic still runs. Never accidentally lose the parent's behaviour.
9
Keep the same signature when overriding. If you need extra parameters, give them defaults so old callers don't break. This preserves polymorphism — every subclass can be called through the parent's interface.
10
When in doubt, ask: "Is this value the same for everyone?" → static variable. "Does this function need no state at all?" → static method. "Does this behaviour differ per subclass?" → override the parent's method. Match the tool to the shape of the problem.