Fizz Buzz

The infamously simple FizzBuzz problem.

Reportedly a high percentage of programmer applicants can't solve this quickly.

Write a program that prints the numbers from 1 to 100. But for multiples of three print “Fizz” instead of the number and for the multiples of five print “Buzz”. For numbers which are multiples of both three and five print “FizzBuzz”.

A deep dive on this problem has been done in jest many times, e.g., deliberate over-engineering or code golf. But in all seriousness, let's consider what's the most Pythonic solution. A truncated version of the common solution:

In [1]:
for num in range(1, 16):
    if num % 5 == 0 and num % 3 == 0:
        print('FizzBuzz')
    elif num % 3 == 0:
        print('Fizz')
    elif num % 5 == 0:
        print('Buzz')
    else:
        print(num)
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

Naturally interview questions tend to focus on output, e.g. print, but that's no reason to skip over basic abstractions or data structures. First, this could be written as a generator, to decouple the print operation and parametrize the numeric range. Alternatively, Python has such strong iterator support that it could also be just a function, ready to be mapped. So let's reframe the basic solution as:

In [2]:
def fizzbuzz(stop):
    for num in range(1, stop):
        if num % 5 == 0 and num % 3 == 0:
            yield 'FizzBuzz'
        elif num % 3 == 0:
            yield 'Fizz'
        elif num % 5 == 0:
            yield 'Buzz'
        else: 
            yield str(num)

' '.join(fizzbuzz(16))
Out[2]:
'1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz'

Even at this size, it's already violating DRY, or the Rule of 3. Clearly the same logic is being repeated with different data.

In [3]:
def fizzbuzz(stop):
    items = (15, 'FizzBuzz'), (3, 'Fizz'), (5, 'Buzz')
    for num in range(1, stop):
        yield next((text for div, text in items if num % div == 0), str(num))

' '.join(fizzbuzz(16))
Out[3]:
'1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz'

However, that variation had to introduce the concept of the least common multiple. Even in such a trivial problem, there's a subtlety in how one interprets requirements. The final directive to output "FizzBuzz" can be seen as a mere clarification of the previous directives; certainly not a coincidence. Making this the more obvious solution:

In [4]:
def fizzbuzz(stop):
    for num in range(1, stop):
        text = ''
        if num % 3 == 0:
            text += 'Fizz'
        if num % 5 == 0:
            text += 'Buzz'
        yield text or str(num)

' '.join(fizzbuzz(16))
Out[4]:
'1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz'

Arguably that insight is more important, because its duplication grows exponentially, not linearly. There's a 2**N sized case statement to handle N cases, luckily N == 2. Adding just one more directive for the number 7 would make the basic solution unwieldy.

And of course both approaches can be combined.

In [5]:
def fizzbuzz(stop):
    items = (3, 'Fizz'), (5, 'Buzz')
    for num in range(1, stop):
        yield ''.join(text for div, text in items if num % div == 0) or str(num)

' '.join(fizzbuzz(16))
Out[5]:
'1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz'

So is that over-engineered? This author would argue that both deduplication and decoupling logic from data are worth observing. So maybe at this size the final version isn't necessary, but surely the basic version is not the most Pythonic.