Python Part I · Module 11 · Functions
Returning More Than One Value
A function that scans a log usually has more than one thing to say: how many lines it read and how many were errors, or the first failure and its position. Chapter 11.8 ruled out printing the extras, and inventing a container type for a two-field answer is far too much ceremony for the job. Python's answer looks at first like a special feature — a function that returns two things, and a call site that catches two names — and it is worth knowing that it is not a feature at all. Once you see what return total, passed actually builds, the call site stops being syntax to memorise and becomes something you can already read.
Foundation15 min readPythonPart IFundamentalsFunctionsTuples
Part I · Module 11 · Chapter 11.9 · Returning More Than One Value
1. One Answer Is Often Not Enough
A summary of a results list genuinely has two numbers in it — how many tests ran, and how many passed. Returning one of them and printing the other is Chapter 11.8's mistake, and returning only one makes the function answer half the question.
2. return With a Comma
def summarise(results):
total = len(results)
passed = 0
for status in results:
if status == "PASS":
passed += 1
return total, passed
total, passed = summarise(["PASS", "FAIL", "PASS"])
print(total, passed)
print(passed, "of", total, "passed")3 2
2 of 3 passedTwo names on the left of the =, two values on the right of the return. The function answered both halves of the question and the caller took them apart in one line.
3. There Is No "Multiple Return" Feature
It is worth being precise, because the syntax invites a wrong model. Python returns exactly one value, always. return total, passed does not return two things — it builds a tuple and returns that.
Catch it without unpacking and it is visible:
def summarise(results):
total = len(results)
passed = 0
for status in results:
if status == "PASS":
passed += 1
return total, passed
pair = summarise(["PASS", "FAIL", "PASS"])
print(pair)
print(type(pair))
print(pair[0], pair[1])
print(len(pair))(3, 2)
<class 'tuple'>
3 2
2A tuple, with everything Chapter 6.1 gave it — indexable, measurable, printable. The parentheses that normally make a tuple visible are optional here, exactly as Chapter 6.1 said; the comma is what builds it.
And the call site is not special syntax either. total, passed = summarise(...) is Chapter 6.5's tuple unpacking, applied to whatever the call produced:
pair = (3, 2)
total, passed = pair
print(total, passed)3 24. The Count Must Match
Because it is ordinary unpacking, the usual mismatch errors apply:
def summarise(results):
return len(results), 0
total, passed, skipped = summarise(["PASS"])ValueError: not enough values to unpack (expected 3, got 2)def summarise(results):
return len(results), 0
total = summarise(["PASS"])
print(total + 1)TypeError: can only concatenate tuple (not "int") to tupleThe second is worth reading twice. Nothing was unpacked, so total holds the whole tuple (1, 0) — and + 1 is a type error rather than arithmetic. A name that looks like it holds a number and actually holds a tuple is the usual symptom of a forgotten second name.
5. Every Path Must Return the Same Shape
A function with several return statements must give back the same number of fields on every one of them. This one does not, and the consequence is the nastiest kind — it works on the first call:
def scan(log):
for line in log:
if "UVM_ERROR" in line:
return line, 1
return ""
message, count = scan(["UVM_ERROR @1: a"])
print("found:", message, count)
message, count = scan(["UVM_INFO @1: ok"])
print("this line is never reached")found: UVM_ERROR @1: a 1
ValueError: not enough values to unpack (expected 2, got 0)The first call found an error, took the two-field path, and unpacked cleanly. The second found nothing, fell through to return "", and handed back a bare string where the caller expected a pair.
Read the message closely: expected 2, got 0. Not "got 1" — the empty string unpacks to zero items, because unpacking a string takes it apart character by character and there are no characters. A one-character return would have said "got 1".
The fix is to make the shapes match:
def scan(log):
for line in log:
if "UVM_ERROR" in line:
return line, 1
return "", 0
print(scan(["UVM_ERROR @1: a"]))
print(scan(["UVM_INFO @1: ok"]))('UVM_ERROR @1: a', 1)
('', 0)Both paths now return two fields, and the not-found case says so in the data — an empty message and a count of zero.
6. Ignoring a Field
When only some of the returned fields are wanted, the convention is to unpack into _:
def scan(log):
first_error = ""
position = -1
for index, line in enumerate(log):
if "UVM_ERROR" in line:
first_error = line
position = index
break
return first_error, position
message, _ = scan(["INFO a", "UVM_ERROR b", "INFO c"])
print("message:", message)
_, where = scan(["INFO a", "UVM_ERROR b", "INFO c"])
print("position:", where)message: UVM_ERROR b
position: 1_ is not syntax. It is an ordinary name, chosen by convention to mean I am required to name this and I am not going to use it — and a reader recognises it instantly. The value is still assigned; nothing is discarded at run time.
Note enumerate doing exactly the job Chapter 10.9 gave it, and break from Chapter 10.7 stopping at the first match.
7. Two or Three Fields, Not Five
The idiom scales badly, and the reason is worth stating because it decides when to stop:
def analyse(log):
return len(log), 2, 1, 0, "FAIL"
total, errors, fatals, warnings, status = analyse(["a", "b"])
print(total, errors, fatals, warnings, status)2 2 1 0 FAILIt runs. Now read the return line on its own: return len(log), 2, 1, 0, "FAIL". Which number is which? The answer lives only in the order, and the order is recorded nowhere except the call site — which is several lines away and may be in another file.
Worse, adding a sixth field or reordering two silently breaks every caller, because the unpacking still succeeds and the names now point at the wrong values.
At that size, return a dictionary — Chapter 11.4 Section 6's shape:
def analyse(log):
return {"total": len(log), "errors": 2, "fatals": 1, "warnings": 0, "status": "FAIL"}
result = analyse(["a", "b"])
print(result["status"], result["errors"])
print(result)FAIL 2
{'total': 2, 'errors': 2, 'fatals': 1, 'warnings': 0, 'status': 'FAIL'}Each field is named at both ends. Adding a sixth breaks nothing, and reordering is meaningless because position carries no information — which is Chapter 8.1's entire argument for dictionaries, arriving as a return value.
| Fields | Return | Why |
|---|---|---|
| 1 | the value | nothing to decide |
| 2–3 | a tuple | the order is obvious and the call site reads cleanly |
| 4+ | a dictionary | positional meaning stops being readable or safe |
| 2–3, but unrelated | a dictionary | if you cannot name the pair, the caller cannot either |
The last row is the judgement one. return total, passed is fine because total and passed is a natural pair. return name, timeout usually means the function is doing two jobs.
8. The Same Mechanism Swaps Two Names
A familiar one-liner is the same tuple construction and unpacking, with no function involved:
a = "first"
b = "second"
a, b = b, a
print(a, b)second firstThe right-hand side builds the tuple ("second", "first") before any assignment happens, then unpacks it into the two names. That is why no temporary variable is needed, and why this is not a special swap operator — it is Section 3's two ideas again.
9. Predict, Run, Observe
# A
def pair():
return 1, 2
print("A:", pair(), type(pair()))
# B
def pair2():
return 1, 2
x, y = pair2()
print("B:", x + y)
# C
def three():
return "a", "b", "c"
first, _, third = three()
print("C:", first, third)
# D
def one():
return 42,
print("D:", one(), len(one()))The verified output:
A: (1, 2) <class 'tuple'>
B: 3
C: a c
D: (42,) 1A — the bare comma built a tuple.
B — unpacked into two names and added.
C — the middle field was assigned to _ and ignored by convention.
D — the trailing comma makes a one-element tuple, exactly as Chapter 6.2 warned. return 42, and return 42 are different functions, and the comma is easy to leave in by accident.
10. Exercises
Exercise 1 — two answers. Write scan(log) returning the number of lines and the number of errors. Call it, unpack both, and print a sentence using each.
Exercise 2 — prove the tuple. Call your Exercise 1 function without unpacking. Print the result, its type, its length and its first element.
Exercise 3 — the mismatch. Unpack a two-value return into three names and record the error. Then into one name, add 1 to it, and record that error. Explain what the second name actually held.
Exercise 4 — ignore one. Write a function returning (name, seed, status) and three calls that each use only one of the fields, using _ for the rest.
Exercise 5 — tuple or dict. For each, say which to return and why: (a) a count and a total (b) a test name, seed, simulator, runtime and verdict (c) a minimum and a maximum (d) a parsed line's severity, timestamp, component and message.
Exercise 6 — break a caller. Take a three-field tuple return and swap two fields in the return line without touching the call site. Show that it still runs and now reports wrongly. Then convert it to a dictionary and repeat the experiment.
Exercise 7 — swap. Swap two names in one line, then write the version with a temporary variable. Explain in two sentences why the one-line form works.
Exercise 8 — the stray comma. Write a function ending return count, and a caller doing n = f() then n + 1. Record the error and say which character caused it.
11. Summary
- Python returns exactly one value.
return a, bbuilds a tuple and returns that. - The comma builds the tuple, not the parentheses — Chapter 6.1's rule.
- The call site is ordinary tuple unpacking from Chapter 6.5, not special syntax.
- So there is no limit to learn: one value, which may hold as many fields as you like.
- The number of names must match, or it is a
ValueError. - Forgetting to unpack leaves a tuple in a name that looks numeric, and the symptom is a
TypeErroron the next arithmetic. - Every
returnin the function must give back the same number of fields. A path that returns a bare value works until the first call that takes it, then raisesValueError. _is a conventional name for a field you must name but will not use. It is not syntax.- Two or three fields are fine as a tuple; four or more want a dictionary, because positional meaning stops being readable and reordering silently breaks callers.
- If you cannot name the pair, it is probably two jobs.
a, b = b, ais the same mechanism — the right side is built before anything is assigned.
The next chapter asks where the names inside a function actually live.
Where this fits
Part of the Python curriculum.
