Dragon

Fields, Statics, and Constants

Defining a class showed per-instance fields. But some state belongs to the class, not to any one object - a shared counter, a configuration constant, a utility that needs no instance at all. This chapter covers the distinction between instance and class-level state, the mutable-default trap that distinction quietly defuses, and the static methods that hang off a class directly.

Instance fields vs. class fields

This is a decision Python gets wrong, and it's worth slowing down for. A field declared in the class body with a default - x: T = value - is a per-instance default: every object gets its own fresh copy. A field marked static is shared across all instances - it lives once, at the class level, and you reach it through the class name.

class Counter {
    static total: int = 0      # shared by every Counter
    count: int = 0             # fresh per instance

    def(name: str) {
        self.name = name
    }

    def tick() -> None {
        self.count = self.count + 1
        Counter.total = Counter.total + 1
    }
}

a: Counter = Counter("a")
b: Counter = Counter("b")
a.tick()
a.tick()
b.tick()
print(a.count)        # 2  - a's own counter
print(b.count)        # 1  - b's own counter
print(Counter.total)  # 3  - shared across both

count is independent per object; total is one value reached through the class name, Counter.total.

The mutable-default trap, defused

Here's the payoff. In Python, a mutable default written in the class body (or as a function default) is created once and silently shared by every instance - the infamous footgun where two objects end up mutating the same list. Dragon evaluates a non-static default fresh for each instance, so the trap simply doesn't exist:

class Bag {
    items: list[str] = []      # fresh empty list per Bag

    def(label: str) {
        self.label = label
    }

    def add(x: str) -> None {
        self.items.append(x)
    }
}

a: Bag = Bag("a")
b: Bag = Bag("b")
a.add("apple")
a.add("pear")
b.add("fig")
print(a.items)   # ['apple', 'pear']
print(b.items)   # ['fig']  - not shared with a

In Python this prints ['apple', 'pear', 'fig'] for both bags. In Dragon, each Bag gets its own list. If you genuinely want shared state, that's what static is for - and you have to ask for it by name.

Constants on a class

Combine static with const for a class-level constant - declared once, and any reassignment is a compile error:

class Circle {
    static const PI: float = 3.14159

    def(r: float) {
        self.r = r
    }

    def area() -> float {
        return Circle.PI * self.r * self.r
    }
}

c: Circle = Circle(2.0)
print(c.area())      # 12.56636
print(Circle.PI)     # 3.14159

const is enforced at compile time and works at any scope - module, function, or (with static) class. See Variables, Constants, and Statics for the full story on const.

Static methods

A method that doesn't touch instance state takes no self, and isn't called on an instance. Mark it with @staticmethod (the Python spelling) or static def (the .dr spelling) - both compile to a plain function living in the class's namespace:

class MathUtils {
    @staticmethod
    def add(a: int, b: int) -> int {
        return a + b
    }
}

print(MathUtils.add(2, 3))   # 5

The static def form shines for factory methods - alternate constructors that return a fresh instance:

class Temperature {
    def(celsius: float) {
        self.celsius = celsius
    }

    static def from_fahrenheit(f: float) -> Temperature {
        return Temperature((f - 32.0) * 5.0 / 9.0)
    }

    def show() -> str {
        return f"{self.celsius}C"
    }
}

t: Temperature = Temperature.from_fahrenheit(212.0)
print(t.show())   # 100.0C

Temperature.from_fahrenheit(...) is called on the class, not an instance, and hands back a constructed Temperature. This is Dragon's preferred answer to the "alternate constructor" patterns Python reaches for @classmethod to express.

At a glance

You want to...Write
A per-instance fieldcount: int = 0 (fresh per object)
A shared class fieldstatic total: int = 0Counter.total
A class constantstatic const PI: float = 3.14159
A function on the class@staticmethod def add(a: int, b: int) -> int
A factory / alternate constructorstatic def from_x(...) -> T { return T(...) }

Shared and per-instance state in hand, the next chapter builds hierarchies of classes - Inheritance and Virtual Dispatch.