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
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
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.