Stop waving the wand of magic numbers

edA‑qa mort‑ora‑y - Feb 25 '19 - - Dev Community

37. You have no idea what that number is, do you? A number without context nor a label is a random value. It doesn't tell us anything. Imagine walking by a billboard, with a picture of a person on a boat, and the text is a giant number 89. I'd be intrigued, but utterly confused. We rightfully reject meaningless numbers...

...so why then do they appear so often in source code? Any time a constant appears in an expression, like 12, we call it a magic number. Through some dark craft, they arise out of the ether and populate our code. We don't know where they came from, what they mean, or how they got there.

Though I don't go down to the level of this article, my book has a chapter about interviewing as an essential programmer skill. Something simple like fixing magic numbers can leave a positive impression on the interviewer.

An example of magic

When I conduct interviews, I ask the candidate to create a deck of cards. I let them simplify it, by using sequentially numbered cards, instead of suits and ranks. In the majority of code I'm presented a loop of this form:

for i in 0..52:
    cards.add( i )
Enter fullscreen mode Exit fullscreen mode

The number 52 doesn't convey any information. Only because we're talking about a card game, and the candidate has assumed a standard poker deck, does the number 52 appear. There are of course many card games that don't have 52 cards.

I request they deal the cards out to multiple players. I accept splitting the deck as well. Frequently, I get code like this:

for i in 0..26:
    player1.add( cards[i] )
for i in 26..52:
    player2.add( cards[i] )
Enter fullscreen mode Exit fullscreen mode

It's not immediately obvious that this is splitting the deck in half. I have to reconstruct that in my head. Making a mistake while writing is easy. What if instead, you saw this code:

for i in 0..27:
    player1.add( cards[i] )
for i in 28..51:
    player2.add( cards[i] )
Enter fullscreen mode Exit fullscreen mode

Is the code accounting for some inclusive/exclusive end condition? Is there a bunch of off-by-one errors in there? What is 51? It doesn't line up with any number I know at all, not even about cards.

Magic number

In this code:

for i in 0...52:
Enter fullscreen mode Exit fullscreen mode

The 52 is known as a magic number. A future coder has no information about what it is. These types of numbers convey no information as to their purpose.

It's easy to get rid of them. Give the numbers a name.

num_cards_in_deck = 52
for i in 0..num_cards_in_deck:
Enter fullscreen mode Exit fullscreen mode

We've immediately improved the quality of this code. A reader knows what the number 52 means. The loop now logically does something for every card in a deck.

Giving a name to a number removes its magical quality. Here we've done it beside the code, but typically symbols like num_cards_in_deck end up as global constants. They are context-free facts. If multiple pieces of code need the same value, then create one constant and share it.

Another response to the dilemma is to add a comment, such as # create a standard size deck to the loop. Just say no to this! Comments are a tool of last resort. They have no structure, can't be enforced by the compiler, and inevitably become out-dated. Using proper symbol names is superior -- code trumps prose.

With our new constant, we can change the second loops:

for i in 0..(num_cards_in_deck/2):
    player1.add( cards[i] )
for i in (num_cards_in_deck/2)..num_cards_in_deck:
    player2.add( cards[i] )
Enter fullscreen mode Exit fullscreen mode

Again, the quality of the code has improved considerably. These loops now have meaning; it's easy to see we're treating the deck as two halves. It's also much less likely to have an error as we're letting the compiler do the division. Moreover, if the number of cards changes, this loop is still correct.

I begrudgingly use this example only because it comes up in my interviews. These two loops are still bad. From a defensive programming standpoint, you shouldn't use a constant value at all. You should use len(cards) and len(cards)/2. Many magic numbers can be removed in this fashion. Rather than tying your code to an arbitrary value, base it on some other knowledge you already have. In this case, we have a cards collection which has all the information we need.

Put the wand down

Not all constants are magic numbers though. There are some special cases, in particular, 1. Adding 1 to a number creates its natural successor. There'd be no confusion about what is happening. There are more, but without seeing the code in particular, I don't wish to give general exceptions.

Even a number like 2 may not seem magical, but can nonetheless be improved upon. In the example I've given, it's clear that it splits something in half, but it doesn't convey what half means. It'd be better to say num_players in that example. Many times, even if the number is obvious, it helps to give it a name. They add clarity to the code.

As fun as magic may be, it's time to put the want down and stop conjuring these numbers into our code.


Read my book What is Programming? and learn what it takes to be a great programmer. I look at the People, the reason software exists, the code at the heart of that software, and you, the person behind the keyboard.


Addendum: A better dealing loop

The dealing loops shown above are still wrong in my opinion. Though not related to magic numbers anymore, here is code that does the dealing between players in a cleaner fashion. It avoids duplicate loops.

for i in len(cards):
    player_cards[i % num_players].add( cards[i] )
Enter fullscreen mode Exit fullscreen mode

Another possible option, which closer mimics dealing, and avoids ranges entirely. It also uses an OOP player that contains cards. This type of code is suitable when you need to model all the states visually, or when there is partial dealing involved. It retains intermediate states.

cur_player = 0
while len(cards) != 0:
    player[cur_player].cards.add( cards.pop() )
    cur_player = (cur_player + 1) % num_players
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player