0
\$\begingroup\$

Where should i put print statements describing the flow of the program? Is there any convetion or code style for this?

Should they be in higher level functions?

def create_collage():
    print('finding images ...')
    image_paths = get_image_paths()

    print('loading images ...')
    images = load_images(image_paths)

    print('assembling output image ...')
    output = assemble_output(images)

    ...

Or in lower level functions?

def get_image_paths():
    print('finding images ...')
    ...


def load_images(image_paths):
    print('loading images ...')
    ...


def assemble_images(images):
    print('assembling output image ...')
    ...

```
\$\endgroup\$
4
\$\begingroup\$

This is a basic but important question.

Most functions can be arranged to operate purely in the realm of computation: they take data as input, return data as output, and have no side effects in the "real world". Examples of side effects would include interacting with the file system or a database, getting user input, and printing. Basically anything that exists outside of the tidy confines of the computer's "brain".

Functions with side effects tend to have trickier failure scenarios and they are harder to test in an automated way (for example, with typical unit testing frameworks) without jumping through various hoops.

That's why most people advise you to aim for a design where side effects like printing end up in a thin outer layer of your program (for example, in main() or a close sibling) rather than in lower-level, computation-heavy functions. Keep as much of your code base as you can in the realm of pure computation. Gary Burnhardt's talk on this subject is excellent.

Of course, every best practice has its exceptions. Don't become a slave to abstract rules. For example, programs that are designed to run persistently in response to requests (such as a web service) might have very strong reasons to want printing in many locations -- for debugability, auditing, and other reasons. In those situations, you definitely want to use a logging framework. Those frameworks have configuration options that allow them to play nicely with automated testing and other development needs. But even in that use case where we have decided to allow side effects deeper than the top level, the same general principles apply: put the logging as high in the call stack as you can, consistent with other needs. For example, in a classic web service, my default would be to put logging in the methods directly handling incoming requests, not in various utility/helper functions than can be focused purely on data-oriented computation. Drawing those lines is a judgment call, of course. And, as shown in Burnhardt's demo, sometimes initial judgements are incorrect, and a design can be refactored without too much difficulty to narrow the proportion of a code base that deviates from pure data-oriented computation.

The main point is to avoid casually scattering side effects across your code base. Allow side effects only when you don't have an alternative. And if you can wrap the side effect in a tool designed to lessen the costs (such as a logging framework) do that as well.

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.