skip to content

Static vs Instance Members

What belongs to the class itself versus each instance, and why static state is effectively global state. A classic screen for whether you understand ownership of state and its testability cost.

on this pageshow

questions

3

Some languages make the class itself an object that can be stored in a variable, passed as an argument and sent messages; others treat class-level (static) members as namespaced functions with no receiver at all. Compare what a first-class class object buys you, and what the receiver-less model forces you to build instead.

level: middleimportance: should knowfreq 42%

answer

  1. receiver or namespace?
  2. Python cls = the class you called through
  3. Smalltalk metaclass mirrors the instance side
  4. Kotlin companion is an object, not a static
  5. no receiver -> inject a factory

basics

~20 s

Where the class is itself an object — Smalltalk, Ruby, Objective-C, Python's classmethod — class-level code has a receiver, so it is inherited, dispatched, and passable as a value. Java, C++ and C# statics have no receiver, so you inject factory objects or use reflection.

solid answer

~60 s

The dividing line is whether class-level code has a **receiver**. - **Smalltalk**: classes are objects with a parallel metaclass hierarchy, so `self new` inside a class-side method builds whatever class received the message; class-side template methods work like instance-side ones. - **Python**: `@classmethod` gets `cls` bound to the class the call went *through*, so `Sub.from_json(...)` yields a `Sub` with nothing written in `Sub`; `@staticmethod` gets nothing and is pure namespacing — the two decorators exist because the receiver is the whole difference. - **Ruby**: `def self.create` lives in the singleton class, which sits in the ancestry chain, so class-level behaviour is refinable per subclass; you pay in singleton-class mental model. - **Objective-C**: `+` methods travel the same message send as `-` methods, and `Class` is a storable value. - **Kotlin/Scala** have no statics: a companion `object` is an ordinary singleton that can implement an interface — but it is not inherited by subclasses. - **Java/C#/C++** bind from the written class name, so the above is unavailable: inject a factory, or go through reflection to get a class value back.

code

python · 9 lines
python
class Base:
    @classmethod
    def create(cls):
        return cls()

class Sub(Base):
    pass

type(Sub.create())   # <class 'Sub'>  -- cls is Sub

go deeper

for a junior

Know that class-level members belong to the type, not to any instance, and that in Java and C# they are selected from the class name you write.

for a middle

Be able to name at least two languages where class-level code has a receiver (Python classmethod, Ruby, Smalltalk, Objective-C) and explain the alternative-constructor consequence.

for a senior

Explain why the receiver-less model pushes you towards injected factories and why class-level calls are a testing seam problem in Java but not in Python or Ruby.

for a principal

Frame it as a language-design trade-off: class-side dispatch buys polymorphic construction and plugin registries at the cost of metaclass complexity and weaker static reasoning; choose per codebase rather than per habit.

## What "class-level" actually means Every object-oriented language distinguishes state and behaviour that belong to *each instance* from state and behaviour that belong to *the type as a whole*. What the languages do not agree on is whether "the type as a whole" is itself an object. That single decision determines almost everything else about class-level members: whether they can be inherited, whether they can be overridden, whether they can be passed to a function, and whether you need a dependency-injection container to work around them. ## Model A: the class is an object In Smalltalk the class `Rectangle` is a genuine object, an instance of its metaclass `Rectangle class`, and the metaclass hierarchy runs parallel to the instance hierarchy. A class-side method is an ordinary method whose receiver happens to be a class. Because there is a receiver, everything that is true of instance-side dispatch is true here: a subclass inherits its parent's class-side methods, can override them, and `self` inside a class-side method refers to the class that actually received the message. That is why `self new` inside an inherited class-side constructor produces an instance of the *subclass*. Ruby reaches the same place by a different route: `def self.create` defines a method on the object's singleton class, and singleton classes participate in the ancestry chain, so class-level methods are inherited and overridable. Objective-C makes `+` methods go through `objc_msgSend` exactly like `-` methods, and `Class` is a first-class value you can put in a dictionary — the basis of runtime class registries. Python splits the idea explicitly. A `@classmethod` receives `cls`, bound to the class the call went through, not the class where the method was written; a `@staticmethod` receives nothing at all and is only namespacing. This is why alternative constructors in Python are written as classmethods: `Sub.from_json(data)` returns a `Sub` even though only `Base.from_json` exists. ## Model B: class-level members are namespaced functions Java, C# and C++ resolve a static call from the type name written at the call site. There is no receiver, so there is nothing to dispatch on: the call is bound before any object exists. Kotlin and Scala removed statics entirely, but their replacement is not model A either — a companion `object` or a Scala `object` is a real singleton instance that can implement an interface and be passed around, yet it is *not* inherited by subclasses, so you get first-class-ness without class-side inheritance. ## What a class object buys 1. **Polymorphic construction.** A base type can define "build one of me from this input" once, and every subtype gets a correct version for free (Python `cls()`, Smalltalk `self new`). 2. **Class-side template method.** Shared class-level workflow with per-subclass hooks, using ordinary overriding. 3. **Types as values.** A registry mapping a tag to a class, a plugin table, a serializer lookup — all trivially expressible where `Class` is a value (Objective-C, Ruby, Python), and requiring reflection where it is not. 4. **Per-class state with real scoping.** Ruby and Smalltalk distinguish state owned by each class object from state shared across the hierarchy; in Java there is only one storage location, on the declaring class. ## What the receiver-less model forces Because a static cannot vary by subtype, the variation has to move into an object you pass: a factory or strategy instance, injected by the caller or a container. This is not a workaround people invented for style reasons — it is the direct consequence of losing the receiver. The same absence is why mocking class-level calls in Java or C# needs bytecode rewriting, while in Python or Ruby you replace an attribute on the class object. ## The trade-offs going the other way Model A is not free. Metaclasses are an extra layer of concepts and a real source of confusion; class-side state that is inherited by reference surprises people; and because any class-level call may be overridden somewhere, static analysis and whole-program optimisation get harder. Model B's binding-by-name is trivially inlinable and trivially readable: you know from the source exactly which body runs. ## Practical guidance Use class-level members for behaviour that genuinely has no per-instance identity and no plausible per-subtype variation. The moment you want variation, the question is not "can I override a static" but "which model does my language give me": in Python or Ruby, reach for a class-level method with a receiver; in Java, C# or C++, stop using a static and pass an object.

  • If class-level members have a receiver, where does per-class state live, and do subclasses share it?
    It depends on the language's scoping of that state. In Ruby, a class-instance variable belongs to each class object separately, while a class variable (@@) is shared across the whole hierarchy — two different scopes with different syntax. In Python, class attributes are read along the MRO, so a subclass sees the base's value until it assigns, at which point a new attribute is created on the subclass. In Java or C# there is exactly one storage slot, owned by the declaring class, no matter how many subclasses read it.
  • Why is a class-level call harder to substitute in tests in Java than in Python?
    In Java the call is bound to a named class at compile time, so there is no seam: replacing it requires bytecode rewriting, a classloader trick, or refactoring the call into an injected object. In Python the class is an object and the method is an attribute on it, so a test can rebind that attribute for the duration of the test. The Java answer is to remove the static from the collaboration path rather than to fight the binding.

saying these in an interview costs you the question

  • Saying class-level methods are 'just methods that do not need an object' and assuming every language resolves them identically.
  • Claiming class-level behaviour can never be polymorphic — true in Java and C++, false in Smalltalk, Ruby, Objective-C and for Python classmethods.
  • Treating a Kotlin companion object or a Scala object as a synonym for static, then expecting subclasses to inherit it.
  • Using Python's @staticmethod where an alternative constructor is meant, losing the cls that makes subclasses work.
  • Reaching for reflection to simulate a class value when the real fix is to pass a factory object.

context

open as a page

Mutable class-level state is a global variable wearing a class name. Compare how different languages' initialization and isolation models change how dangerous that is, and what each of them actually gives you for testing code that depends on it.

level: seniorimportance: should knowfreq 48%

basics

~20 s

Class-level mutable state has process-wide lifetime and no owner. Languages diverge sharply: Rust forces a OnceLock or Mutex, C# gives each closed generic type its own copy while Java's erasure gives one, Python lets a test rebind the attribute, Go offers no reset hook, Elixir has no such state at all.

open as a page

Generic code often needs an operation that belongs to a type rather than to any instance — 'produce the identity value', 'parse one of these from text', 'give me an empty builder'. Some languages let an interface or constraint require such a member; others cannot express it at all. Compare the designs and the price each one pays.

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Requiring a type-level operation means dispatching without a receiver. Haskell picks the implementation from the expected result type via dictionaries; Rust and Swift allow such requirements but the trait or protocol stops working as dyn/existential; C# 11 adds static abstract members usable only through a type parameter; Java must hand in a factory object.

open as a page