This article contains affiliate links. See my affiliate disclosure for more information.



Hang out with Python devs long enough, and you'll hear all about Tim Peter's Zen Of Python.

The Zen, which you can conveniently read by executing import this in a Python REPL, presents 19 of the 20 guiding principles behind Python's design. Recently I've come to appreciate one aphorism more than the others: "Explicit is better than implicit."

The most common interpretation I've seen — so common that it currently inhabits Google's featured snippet when searching for the phrase — is that verbose code is better than terse code because verbosity is, apparently, the key to readability... or something.

Sure, using better variable names and replacing magic numbers with named constants (or, in Python's case, "constants") are all great things. But when was the last time you hunted down implicit inputs in your code and made them explicit?

✉️
This article was originally published in my Curious About Code newsletter. Never miss an issue. Subscribe here →

How To Recognize Implicit Inputs and Outputs

How many inputs and outputs does the following function have?

def find_big_numbers():
    with open("numbers.txt", "r") as f:
        for line in f:
            if line.strip().isnumeric():
               number = float(line)
               if number > 100:
                   print(number)

find_big_numbers() has no parameters and always returns None. If you couldn't see the function body and couldn't access the standard output stream, would you even believe that this function does anything?

And yet, find_big_numbers() has two inputs and another output besides None:

  • numbers.txt is an implicit input. The function won't work without it, but it is impossible to know that the file is required without reading the function body.
  • The magic number 100 on line 6 is an implicit input. You can't define a "big number" without it, but there is no way to know that threshold without reading the function body.
  • Values may or may not print to stdout, depending on the contents of numbers.txt. This is an implicit output because the function does not return those values.

Implicit outputs are often called side effects.

Try It Yourself

Identify all of the inputs and outputs of the is_adult function in this code snippet:

from datetime import date

birthdays = {
    "miles": date(2000, 1, 14),
    "antoine": date(1987, 3, 25),
    "julia": date(2009, 11, 2),
}

children = set()
adults = set()

def is_adult(name):
    birthdate = birthdays.get(name)
    if birthdate:
        today = date.today()
        days_old = (today - birthdate).days
        years_old = days_old // 365
        if years_old >= 18:
            print(f"{name} is an adult")
            adults.add(name)
            return True
        else:
            print(f"{name} is not an adult")
            children.add(name)
            return False
🤔
There is more wrong with this code than just implicit inputs and outputs. What else would you do to clean this up?



Why You Should Avoid Implicit Input And Output

One good reason is their penchant for violating the principle of least surprise.

Of course, not all implicit inputs and outputs are bad. A method like .write(), which Python file objects use to write data to a file, has an implicit output: the file. There's no way to eliminate it. But it isn't surprising. Writing to a file is the whole point.

On the other hand, a function like is_adult() from the previous code snippet does lots of surprising things. Less extreme examples abound.

💡
It's a good exercise to read through some of your favorite library's code on GitHub and see if you can spot the implicit inputs and outputs. Ask yourself: do any of them surprise you?

Avoiding implicit input and output also improves your code's testability and re-usability. To see how, let's refactor the find_big_numbers() function from earlier.


How To Remove Implicit Input And Output

Here's find_big_numbers() again so you don't have to scroll up:

def find_big_numbers():
    with open("numbers.txt", "r") as f:
        for line in f:
            if line.strip().isnumeric():
               number = float(line)
               if number > 100:
                   print(number)

Earlier, we identified two implicit inputs, the numbers.txt file and the number 100, and one implicit output, the values printed to stdout. Let's work on the inputs first.

You can move the file name and the threshold value to parameters of the function:

def find_big_numbers(path, threshold=100):
    with open(path, "r") as f:
        for line in f:
            if line.strip().isnumeric():
               number = float(line)
               if number > threshold:
                   print(number)

This has already dramatically improved the testability and re-usability. If you want to try it on a different file, pass the path as an argument. (As a bonus, the file can now be anywhere on your computer.) You can also change the threshold for "big numbers," if needed.

But the output is hard to test.

If you want to know that the function produced the correct values, you need to intercept stdout. It's possible. But why not just return a list of all of the values:

def find_big_numbers(path, threshold=100):
    big_numbers = []
    with open(path, "r") as f:
        for line in f:
            if line.strip().isnumeric():
               number = float(line)
               if number > threshold:
                   big_numbers.append(number)
    return big_numbers

Now find_big_numbers() has an explicit return statement that returns a list of big numbers found in the file.

🤔
Aside from removing implicit input and output, there is still a lot that could be done to improve find_big_numbers(). How would you go about cleaning it up?

You can test find_big_numbers() by calling it with the path of a file whose contents are known and comparing the list returned to the list of correct values:

# test_nums.txt looks like:
# 29
# 375
# 84

>>> expected_nums = [375.0]
>>> actual_nums = find_big_numbers("test_nums.txt")
>>> assert(actual_nums == expected_nums)

find_big_numbers() is more reusable now, too. You aren't limited to printing the numbers to stdout. You can send those big numbers wherever you want.

🧠
Let's recap:

Implicit inputs are data used by a function or program that aren't explicitly passed as arguments. You can eliminate implicit inputs by refactoring them into parameters.

Implicit outputs are data sent somewhere external to the function or program that aren't explicitly returned. You can remove explicit outputs by replacing them with suitable return values.

Not all implicit input and output can be avoided, such as functions whose purpose is to read or write data from files and databases or to send an email. Still, eliminating as many implicit inputs and outputs as possible improves the testability and re-usability of your code.
🤔
So here's the question: Did we remove all of the implicit inputs and outputs from find_big_numbers()?


Curious about what happened to the 20th line in the Zen of Python? There are all sorts of theories floating around the internet. This one strikes me as reasonably likely.


I used plenty of implicit input and outputs as a begging — even intermediate! — programmer. Here are four other mistakes I made:

4 Things I Wish I’d Done Earlier As a Coder
And a lesson in reading code from Donald Knuth.

Dig Deeper

Learn more about removing implicit inputs and outputs, as well as other strategies for dealing with complex code in Eric Normand's book Grokking Simplicity.

Get instant access from Manning, or buy a print version from Amazon.