Magic numbers are numerical constants that have no clear meaning in the code and therefore make code harder to read. Anything that makes code harder to read is something we can use to obfuscate our code!
Table of Contents
What Are Magic Numbers?
I was first introduced to the concept of magic numbers when I started teaching. According to the curriculum I was assigned, magic numbers were defined as any number without an obvious meaning or purpose.
Of course, it’s hard for a static analysis tool to infer if a number has meaning to its reader, so most tools define magic numbers as any number other than 0, 1, or 2. That way, it’s up to the user to give that number a meaningful name.
The way this usually plays out in code is that there is some known constant that we use in a formula or as a loop counter. For example, in the Sample Programs repo, we have a solution to the Baklava problem in Python as follows:
for i in range(0, 10, 1): print((" " * (10 - i)) + ("*" * (i * 2 + 1))) for i in range(10, -1, -1): print((" " * (10 - i)) + ("*" * (i * 2 + 1)))
Immediately, without knowing what the Baklava problem is, this code appears to be littered with magic numbers. For instance, the number “10” appears in several places, as do “2”, “1”, “0”, and “-1”.
By the law of magic numbers, we should really be giving these constants names, so the code is much easier to read. Here’s what that might look like:
NUM_ROWS = 10 for i in range(0, NUM_ROWS, 1): print((" " * (NUM_ROWS - i)) + ("*" * (i * 2 + 1))) for i in range(NUM_ROWS, -1, -1): print((" " * (NUM_ROWS - i)) + ("*" * (i * 2 + 1)))
With one additional variable, it becomes a bit more clear what the code is trying to accomplish. Though, I think it could benefit from a few more variables:
NUM_ROWS = 10 for i in range(0, NUM_ROWS, 1): num_spaces = NUM_ROWS - i num_stars = i * 2 + 1 print((" " * num_spaces) + ("*" * num_stars)) for i in range(NUM_ROWS, -1, -1): num_spaces = NUM_ROWS - i num_stars = i * 2 + 1 print((" " * num_spaces) + ("*" * num_stars))
Or even better, a function:
NUM_ROWS = 10 def print_row(i): num_spaces = NUM_ROWS - i num_stars = i * 2 + 1 print((" " * num_spaces) + ("*" * num_stars)) for i in range(0, NUM_ROWS, 1): print_row(i) for i in range(NUM_ROWS, -1, -1): print_row(i)
Regardless, the point being that we can improve the readability of our code by removing magic numbers. Therefore, if we want to make our code harder to read, we can introduce magic numbers! And let me tell you, there are a lot of fun ways to do this.
Making Magic Numbers the Norm
Anywhere in your code that you’re using a numerical constant, it’s time to abandon its name. We already looked at an example of the opposite, so let’s start with a new example. For instance, imagine we had a program which computed some roman numeral math. In this example, we probably have constants for each of the roman numerals:
I = 1 V = 5 X = 10 L = 50 C = 100 D = 500 M = 1000
And then, we could use these constants to compute a list of roman numerals. To do that, we can loop over each value in the list and add or subtract that current value from the total as expected:
number = [I, X, X] i = 0 total = 0 while i < len(number): ... i++
Now, it’s clear we’re dealing with roman numerals, so we can probably compute and verify the result in our heads. Let’s instead remove the constants all together:
number = [1, 10, 10] i = 0 total = 0 while i < len(number): ... i++
Now, it’s completely unclear what this code is even doing. Therefore, mission accomplished!
When The Numbers Aren’t Magic Enough
While magic numbers only slightly make code harder to read, we can do so much worse. For instance, magic numbers are usually in a base we’re used to reading (i.e., base 10). What’s stopping us from using a different base to represent our magic numbers?
number = [0b1, 0b1010, 0b1010] i = 0 total = 0 while i < len(number): ... i++
And because these examples are Python, we can get the binary representations of each number easily: just call the
bin() function on the number. But, why stop there? We can use any of the common bases, such as hexadecimal or octal. Hell, mix and match them to completely lose your readers. I’d definitely recommend doing this for duplicate numbers, so they appear different at a glance:
number = [0b1, 0b1010, 0xa] i = 0 total = 0 while i < len(number): ... i++
Fortunately, languages like Python and Java can handle different bases interchangeably, so this won’t affect your code at all. However, this will really hurt your reader, so why not go all out? Swap all the numbers with different bases:
number = [0b1, 0b1010, 0xa] i = 0o0 total = 0x0 while i < len(number): ... i++
Now that’s some fun looking code! Imagine combining this with the visually similar characters trick. I can already see a function call that looks like this:
Good Luck; Have Fun!
One of the things I’m really enjoying about this series is breaking all of the best practices we’re taught as developers. It’s even more fun as someone who teaches best practices to lean into the memes a bit. Overall, I’m really pleased with where this series is at, and I hope to keep expanding it for some time. Surely, y’all are enjoying it as well!
With that said, let’s call it a day. As usual, if you liked this one, there’s a lot more where that came from:
- Obfuscation Techniques: No More Type Hints
- Obfuscation Techniques: Visually Similar Characters
- Obfuscation Techniques: Shadowing Built-in Functions
In addition, here are some Python resources you might like (#ad):
- Effective Python: 90 Specific Ways to Write Better Python
- Python Tricks: A Buffet of Awesome Python Features
- Python Programming: An Introduction to Computer Science
Finally, you can take your support a step further by heading over to my ways to grow the site. Otherwise, take care!
Recently, I was giving a lecture about Java's "common" methods (i.e., all of the methods of Object), and I had epiphany about how Java only has toString() while Python has str() and repr(). So, it...
Type hinting is a nice tool that dynamic typing languages employ to make code more readable. As you can probably imagine, readability is not the goal with obfuscating code, so we ought to get rid of...