Objects at the Command Line #001 — FizzBuzz Without an `if` Statement

July 23, 2026

FizzBuzz is the interview cliché everyone loves to hate. It’s also a perfect tiny lab for a question I keep coming back to: what does scripting feel like when everything is an object?

Here’s how most of us write it — clear, idiomatic Python, nothing wrong with it:

def fizzbuzz(n):
    if n % 15 == 0:
        return "FizzBuzz"
    elif n % 3 == 0:
        return "Fizz"
    elif n % 5 == 0:
        return "Buzz"
    else:
        return str(n)

for n in range(1, 21):
    print(fizzbuzz(n))

Now the same thing in GNU Smalltalk:

Object subclass: Rule [
    | divisor word |
    Rule class >> on: aNumber say: aString [
        ^self new setDivisor: aNumber word: aString
    ]
    setDivisor: aNumber word: aString [
        divisor := aNumber.
        word := aString
    ]
    appliesTo: n [ ^n \\ divisor = 0 ]
    word [ ^word ]
]

| rules |
rules := { Rule on: 3 say: 'Fizz'. Rule on: 5 say: 'Buzz' }.

1 to: 20 do: [:n |
    | said |
    said := (rules select: [:r | r appliesTo: n])
                inject: '' into: [:acc :r | acc , r word].
    Transcript showCr: (said isEmpty ifTrue: [n printString] ifFalse: [said])
].

Run it (gst after.st) and you get exactly the FizzBuzz you’d expect.

The thing that reframed it for me

Smalltalk has no if keyword. That ifTrue:ifFalse: up there? It’s a message sent to a boolean object. true and false are objects that know how to respond to it. The language didn’t reserve a word for branching — it let objects handle it, like everything else.

Once branching is “just messages,” the design pressure changes. A rule stops being a line in a conditional ladder and becomes a small object that knows two things: which numbers it applies to, and what to say. The number never inspects itself. The rules filter themselves with select:. The words assemble themselves with inject:into:.

Why this isn’t just aesthetics

Add “Bazz on 7.” In the Python ladder you edit the logic — and now you owe every combination: 21, 35, 105. In the Smalltalk version:

rules := { Rule on: 3 say: 'Fizz'. Rule on: 5 say: 'Buzz'. Rule on: 7 say: 'Bazz' }.

One object. No logic touched. The combinations fall out for free, because concatenation of matched words was never special-cased in the first place. That’s the open/closed principle showing up not as a lecture, but as the natural shape of the code.

The transferable lesson

You don’t need Smalltalk for this. The next time you’re adding another branch to a conditional, pause and ask: am I adding logic, or am I adding data? A list of small objects frequently replaces the whole ladder — in Python, JavaScript, Ruby, anywhere — and it stays open for extension without editing what already works.

Honest caveat: for real FizzBuzz, write the three-line version and move on. The object version earns its keep when the rules multiply — pricing tiers, validation chains, discounts, feature flags — the places a conditional ladder quietly becomes a maintenance tax.


Every sample in this series is executed against GNU Smalltalk 3.2.5 before it ships — this one included. I write them in VS Code using my open-source GNU Smalltalk extension. How would you have modeled this — and what should Episode #002 tackle?