Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
3 2
2 of 3 passed

Two 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
(3, 2)
<class 'tuple'>
3 2
2

A 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
pair = (3, 2)
total, passed = pair
print(total, passed)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
3 2

4. The Count Must Match

Because it is ordinary unpacking, the usual mismatch errors apply:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
def summarise(results):
    return len(results), 0


total, passed, skipped = summarise(["PASS"])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ValueError: not enough values to unpack (expected 3, got 2)
Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
def summarise(results):
    return len(results), 0


total = summarise(["PASS"])
print(total + 1)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TypeError: can only concatenate tuple (not "int") to tuple

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
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")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"]))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
('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 _:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
def analyse(log):
    return len(log), 2, 1, 0, "FAIL"


total, errors, fatals, warnings, status = analyse(["a", "b"])
print(total, errors, fatals, warnings, status)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
2 2 1 0 FAIL

It 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

FieldsReturnWhy
1the valuenothing to decide
2–3a tuplethe order is obvious and the call site reads cleanly
4+a dictionarypositional meaning stops being readable or safe
2–3, but unrelateda dictionaryif 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a = "first"
b = "second"

a, b = b, a

print(a, b)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
second first

The 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: (1, 2) <class 'tuple'>
B: 3
C: a c
D: (42,) 1

A — 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, b builds 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 TypeError on the next arithmetic.
  • Every return in 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 raises ValueError.
  • _ 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, a is 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.