Python Part I · Module 11 · Functions
return Is Not print
Two functions can look almost identical, differ by one word, and be worlds apart in usefulness. One prints its answer; the other returns it. On the screen they appear to do the same thing, which is exactly why the mistake is so easy to make and so hard to notice — the first version looks like it works. It does not. A function that prints its answer has not produced an answer; it has produced an effect on your terminal, and the program it was called from got nothing. This is the most consequential distinction in the module, and it is worth more than agreement — it is worth seeing fail in four different ways.
Foundation16 min readPythonPart IFundamentalsFunctionsReturn Values
Part I · Module 11 · Chapter 11.8 · return Is Not print
1. Two Functions That Look the Same
def count_printing(log):
errors = 0
for line in log:
if "UVM_ERROR" in line:
errors += 1
print(errors)
def count_returning(log):
errors = 0
for line in log:
if "UVM_ERROR" in line:
errors += 1
return errors
log = ["UVM_ERROR @1: a", "UVM_INFO @2: b", "UVM_ERROR @3: c"]
count_printing(log)
print(count_returning(log))2
2The same 2 twice. On screen they are indistinguishable, and that is the entire problem — the first version appears to work.
2. One Hands a Value Back; the Other Does Not
print(value) shows something to a HUMAN, on a terminal
return value hands something to the CODE that called the functionThey are not two styles of output. They are output to two completely different audiences, and only one of them is a program.
print writes characters to the terminal and produces no value. return produces a value and writes nothing. A function that prints its answer has told a person and told the program nothing.
3. The Caller Gets None
def count_printing(log):
errors = 0
for line in log:
if "UVM_ERROR" in line:
errors += 1
print(errors)
log = ["UVM_ERROR @1: a", "UVM_ERROR @2: b"]
result = count_printing(log)
print("result is:", result)
print("type:", type(result))2
result is: None
type: <class 'NoneType'>The 2 on the first line went to the terminal. The variable result — the thing the rest of the program would actually use — holds None, because Chapter 11.4 Section 5 established that a function with no return hands back None.
So the number exists on your screen and nowhere else in the program.
4. It Does Not Compose
Chapter 11.3 Section 8 showed that a call is a value, so calls nest. That property vanishes the moment a function prints instead of returning:
def count_printing(log):
total = 0
for line in log:
if "UVM_ERROR" in line:
total += 1
print(total)
a = ["UVM_ERROR @1: a"]
b = ["UVM_ERROR @2: b", "UVM_ERROR @3: c"]
print(count_printing(a) + count_printing(b))1
2
TypeError: unsupported operand type(s) for +: 'NoneType' and 'NoneType'Read the output carefully. The 1 and the 2 printed — both functions ran and both knew their answers. Then the addition failed, and the message names the reason exactly: 'NoneType' and 'NoneType'. Neither call produced a value. The two numbers on screen were never in the program.
The returning version composes without comment:
def count_errors(log):
total = 0
for line in log:
if "UVM_ERROR" in line:
total += 1
return total
a = ["UVM_ERROR @1: a"]
b = ["UVM_ERROR @2: b", "UVM_ERROR @3: c"]
print(count_errors(a) + count_errors(b))
print(count_errors(a) > 0)
print([count_errors(a), count_errors(b)])3
True
[1, 2]Added, compared, collected into a list. None of those three lines can be written against a function that prints.
5. The Answer Cannot Be Kept
A returned value can be stored, accumulated, sorted, compared, or written to a report. A printed one has already left the program.
def count_errors(log):
total = 0
for line in log:
if "UVM_ERROR" in line:
total += 1
return total
logs = {
"axi_smoke": ["UVM_ERROR @1: a", "UVM_ERROR @2: b"],
"axi_burst": ["UVM_INFO @1: ok"],
"reset_seq": ["UVM_ERROR @1: x"],
}
counts = {}
for name in logs:
counts[name] = count_errors(logs[name])
worst = ""
worst_count = -1
for name in counts:
if counts[name] > worst_count:
worst_count = counts[name]
worst = name
print(counts)
print("worst:", worst, "with", worst_count)
print("total:", counts["axi_smoke"] + counts["axi_burst"] + counts["reset_seq"]){'axi_smoke': 2, 'axi_burst': 0, 'reset_seq': 1}
worst: axi_smoke with 2
total: 3Three different uses of the same answers, and the accumulator and max-tracking patterns from Chapter 10.10 doing the work. With a printing version, none of this is reachable — the numbers appeared on screen in a fixed order and were gone.
6. It Cannot Be Reused Where Printing Is Wrong
Some callers must not print at all:
- a script with a
--quietflag - a function assembling a report file rather than terminal output
- a test checking that
count_errorsgives2for a known log - any code that calls it in a loop over four thousand logs, where four thousand stray lines would bury the actual summary
A returning function works in every one of those. A printing function forces its output on all of them, and the only fix is to change the function — which means every other caller is affected too.
7. Where print Is Exactly Right
This chapter is not an argument against print. It is an argument about which layer prints.
def count_errors(log):
total = 0
for line in log:
if "UVM_ERROR" in line:
total += 1
return total
def classify(count):
if count == 0:
return "PASS"
return "FAIL"
logs = {"axi_smoke": ["UVM_ERROR @1: a"], "axi_burst": ["UVM_INFO @1: ok"]}
for name in logs:
count = count_errors(logs[name])
print(name, "->", classify(count), "(" + str(count) + " errors)")axi_smoke -> FAIL (1 errors)
axi_burst -> PASS (0 errors)count_errors and classify compute and return. The loop at the bottom prints — and that is correct, because its entire job is to show a person the result.
The rule, stated properly:
Functions that compute a result return it. One layer at the edge of the program turns results into output.
That edge layer is allowed to print as much as it likes, because printing is what it is for. Chapter 11.15 turns this split into a deliberate architecture; here it is enough to know that "never print in a function" is the wrong lesson and "do not print instead of returning" is the right one.
8. Do Not Do Both
The tempting compromise is to print and return:
def count_errors(log):
total = 0
for line in log:
if "UVM_ERROR" in line:
total += 1
print("counted", total, "errors")
return total
a = count_errors(["UVM_ERROR @1: a"])
b = count_errors(["UVM_ERROR @1: a", "UVM_ERROR @2: b"])
print("total:", a + b)counted 1 errors
counted 2 errors
total: 3It works, and it has quietly reintroduced Section 6's problem. This function still cannot be used by anything that must stay quiet, and calling it four thousand times produces four thousand lines nobody asked for.
The same reasoning as Chapter 11.7 Section 8's rule against mutating and returning: a function should have one effect that the caller can see. If you want progress output during a long run, that is a decision for the calling layer, which can print as it goes around its own loop.
9. The Two Channels, Compared
| Question | print(x) | return x |
|---|---|---|
| Who receives it | a human, on a terminal | the calling code |
| What the caller gets | None | the value |
| Can the result be stored | no | yes |
| Can calls be nested | no | yes |
| Can it be totalled or compared | no | yes |
| Can it be checked by a test | no | yes |
| Usable in a quiet context | no | yes |
| Right place for it | one layer, at the edge | every function that computes |
Every "no" in that column is a thing the function cannot do, permanently, until it is rewritten.
10. Predict, Run, Observe
# A
def p(n):
print(n * 2)
def r(n):
return n * 2
x = p(5)
y = r(5)
print("A:", x, y)
# B
def half(n):
return n // 2
print("B:", half(half(20)))
# C
def shout(t):
print(t.upper())
print("C:", shout("hi"))
# D
def tally(items):
n = 0
for i in items:
n += 1
return n
print("D:", tally([1, 2]) + tally([3]))The verified output:
10
A: None 10
B: 5
HI
C: None
D: 3A — p(5) printed 10 before anything else; its result is None. r(5) printed nothing and its result is 10.
B — nesting works because half returns.
C — two lines from one statement, and neither is what you would expect. shout("hi") ran first and printed HI with no prefix, because that print is inside the function and knows nothing about the "C:" label. Then the outer print showed what shout handed back — C: None. Two lines from one call, with the label attached to the wrong one, is the signature of this bug.
D — two returned values added. The printing equivalent would be the Section 4 TypeError.
11. Exercises
Exercise 1 — convert it. Rewrite this so it can be used by other code, and show a call that would have been impossible before.
def average(numbers):
total = 0
for n in numbers:
total += n
print(total / len(numbers))Exercise 2 — the None. Call a printing function, store its result, and print the stored value and its type. Explain the two lines of output in two sentences.
Exercise 3 — break composition. Write two printing functions and try to add their results. Record the exact error and say which part of the message identifies the cause.
Exercise 4 — keep the answers. Using a returning count_errors, build a dictionary of name to count for three logs, then print only the name with the most errors. Say why the printing version could not do this.
Exercise 5 — find the edge. In a three-function script of your own, identify which function should be the only one that prints. Explain your choice in two sentences.
Exercise 6 — both at once. Take Section 8's function and name two callers for which its printing is a problem. Then move the printing to where it belongs.
Exercise 7 — the quiet test. Write a check that count_errors returns 2 for a known three-line log, printing only ok or wrong. Explain in one sentence why a printing count_errors makes this impossible.
Exercise 8 — classify these. For each, say whether the function should print, return, or both, and why: (a) counts failures in a log (b) renders the final summary table (c) decides a pass/fail verdict from counts (d) shows a progress line every hundred jobs (e) formats a test name and seed into a job id.
12. Summary
printshows a human something;returnhands a value to the calling code. Different audiences, not different styles.- A printing function hands its caller
None. The answer exists on screen and nowhere in the program. - On screen the two versions look identical, which is why the mistake survives the first use.
- A printing function does not compose.
f(a) + f(b)is aTypeErroronNoneType. - Its answer cannot be stored, totalled, compared, sorted or written to a report.
- It cannot be tested, because there is no value to check.
- It cannot be reused anywhere that must stay quiet — a
--quietflag, a report builder, a loop over four thousand logs. - The failure arrives at the second use, not the first, and the fix touches every call site.
- Printing is correct at the edge. Functions that compute return; one layer turns results into output.
- Do not do both. A function that prints and returns is still unusable in a quiet context.
The next chapter removes the last reason people reach for printing: needing to hand back more than one thing.
Where this fits
Part of the Python curriculum.
