Python Part I · Module 11 · Functions
Arguments In, Result Out
A function has two channels to the rest of the program: values go in through its parameters, and one value comes back out through return. Keeping to those two is what makes a function something you can reason about on its own — you can read its header, read its body, and know everything that affects what it does. The moment a function depends on something that arrived by neither route, that stops being true, and this chapter opens with the executed demonstration of what that costs: the same function, given the same argument twice, returning two different answers.
Foundation16 min readPythonPart IFundamentalsFunctionsReturn Values
Part I · Module 11 · Chapter 11.4 · Arguments In, Result Out
1. A Function That Reads Something It Was Not Given
Here is a counter that takes the log as an argument and gets the marker from a name defined above it:
MARKER = "UVM_ERROR"
def count(lines):
n = 0
for line in lines:
if MARKER in line:
n += 1
return n
log = ["UVM_ERROR a", "UVM_ERROR b", "UVM_FATAL c"]
print("first call :", count(log))
MARKER = "UVM_FATAL"
print("second call:", count(log))first call : 2
second call: 1Same function. Same argument. Different answers.
Nothing about the call changed — count(log) is written identically on both lines, and log was never touched. What changed was a name the function reads but never declares, and nothing in the call site gives any hint that it exists.
A reader auditing the second call cannot tell what it will return without searching the whole file for assignments to MARKER. That is the cost: the function's header stops being a complete account of its inputs.
2. Pass It In
def count(lines, marker):
n = 0
for line in lines:
if marker in line:
n += 1
return n
log = ["UVM_ERROR a", "UVM_ERROR b", "UVM_FATAL c"]
print(count(log, "UVM_ERROR"))
print(count(log, "UVM_FATAL"))
print(count(log, "UVM_ERROR"))2
1
2Three calls, three answers that are each decided entirely by what is inside the parentheses. The third call repeats the first and gets the first's answer back, which the Section 1 version could not promise.
The function also became more capable in the process. The version with the hidden name could count one thing; this one counts whatever you ask for.
There is a legitimate exception, and it is narrow: a value that is genuinely constant for the life of the program — a fixed threshold, a fixed banner width — may be read from a module-level name. The distinguishing test is whether anything ever reassigns it. MARKER in Section 1 was reassigned; that is what made it an input in disguise.
3. return Hands a Value Back
return ends the call and supplies the value that the call expression becomes:
def classify(errors, fatals):
if fatals > 0:
return "FATAL"
if errors > 0:
return "FAIL"
return "PASS"
print(classify(0, 0))
print(classify(3, 0))
print(classify(3, 1))PASS
FAIL
FATALThree return statements, one of which runs. This is a legitimate shape — a function may have as many returns as it has outcomes, and the early ones remove the need for elif chains and nesting.
4. return Exits the Function Immediately
Not just the current block, and not just the current loop — the whole call ends:
def first_error(log):
for line in log:
if "UVM_ERROR" in line:
return line
print("checked:", line)
return ""
print(first_error(["INFO a", "UVM_ERROR b", "INFO c"]))checked: INFO a
UVM_ERROR bchecked: printed once, for the one line examined before the match. The loop did not finish, and return "" on the last line was never reached.
def find_pair(tests, seeds, target):
for test in tests:
for seed in seeds:
if seed == target:
return test, seed
return None
print(find_pair(["smoke", "reset"], [4210, 4211], 4211))
print(find_pair(["smoke"], [4210], 9999))('smoke', 4211)
NoneTwo loops, one return, no flag. Chapter 11.9 covers that two-value return properly.
5. No return Means None
A function that never executes a return hands back None:
def shout(text):
print(text.upper())
result = shout("hello")
print("returned:", result)
print(type(result))HELLO
returned: None
<class 'NoneType'>The function did something visible and gave the caller nothing. None is Chapter 2.8's "no value at all", arriving here as "this function has no result".
The same happens with a bare return used only to exit early:
def report(errors):
if errors == 0:
return
print("errors:", errors)
print(report(0))
print(report(3))None
errors: 3
Nonereturn with nothing after it means stop here, with no result — and None is what the caller receives.
This chapter is about the mechanism. Whether a function should print or return is a different question, and Chapter 11.8 is entirely about it, because the answer decides whether the function can be used by other code at all.
6. Returning Several Fields
One return hands back one value — but that value can be a collection, so "one value" is not a limit on how much information comes back:
def parse_log(lines):
errors = 0
fatals = 0
for line in lines:
if "UVM_FATAL" in line:
fatals += 1
elif "UVM_ERROR" in line:
errors += 1
if fatals > 0:
status = "FATAL"
elif errors > 0:
status = "FAIL"
else:
status = "PASS"
return {"errors": errors, "fatals": fatals, "status": status}
result = parse_log(["UVM_ERROR @1: a", "UVM_INFO @2: b"])
print(result)
print(result["status"], result["errors"]){'errors': 1, 'fatals': 0, 'status': 'FAIL'}
FAIL 1A dictionary is the right shape when the fields have names worth keeping — the caller writes result["status"], which says what it means, rather than counting positions. That is Chapter 8.1's whole argument, arriving as a return value.
(The canonical signature for this function is written def parse_log(path) -> dict:. The -> dict part is a type hint, which documents the return type without changing behaviour; Chapter 11.12 covers annotations properly and this chapter leaves them off.)
When the fields have no useful names — a count and a total, say — a tuple is lighter, and Chapter 11.9 covers that.
7. A Function Can Take Nothing and Still Return Something
The two channels are independent. Either can be empty:
def banner():
return "=" * 30
def note(text):
print("note:", text)
print(banner())
note("starting")
print(note("starting"))==============================
note: starting
note: starting
Nonebanner takes nothing and returns something. note takes something and returns nothing — and the third call shows that None arriving where a value was expected.
8. The Two Channels
| In | Out | |
|---|---|---|
| How | parameters, filled by arguments at the call | one return value |
| How many | as many parameters as needed | exactly one value — which may be a collection |
| Declared where | the def header | nowhere; the body decides |
| If omitted | a call with too few arguments is a TypeError | the function returns None |
| Visible to the caller | yes, in the header | yes, as the value of the call |
The row worth noting is the fourth. A missing argument is caught loudly at the call. A missing return is not caught at all — the caller gets None and finds out later, usually when None is used in arithmetic or a lookup. That asymmetry is why Chapter 11.8 matters as much as it does.
9. Predict, Run, Observe
# A
def add(a, b):
return a + b
print("A:", add(2, 3), add(add(1, 1), 1))
# B
def early(items):
for item in items:
return item
return "empty"
print("B:", early([10, 20]), early([]))
# C
def note(t):
print("C: note", t)
x = note("hi")
print("C: x is", x)
# D
def stats(nums):
total = 0
for n in nums:
total += n
return {"count": len(nums), "total": total}
print("D:", stats([1, 2, 3]))The verified output:
A: 5 3
B: 10 empty
C: note hi
C: x is None
D: {'count': 3, 'total': 6}A — a call is a value, so it nests.
B — the first call returns on the very first pass, so only 10 comes back; the second never enters the loop and falls through to return "empty".
C — the function printed and returned nothing, so x is None.
D — one return, two fields, named by their keys.
10. Exercises
Exercise 1 — find the hidden input. This function's result depends on something not in its header. Name it, then rewrite the function so the header is complete.
THRESHOLD = 90
def passed(coverage):
return coverage >= THRESHOLDExercise 2 — prove the drift. Using Section 1's program shape, write a function whose answer changes between two identical calls. Print both answers and explain in two sentences why a reader of the call site could not have predicted the second.
Exercise 3 — early return. Write first_failing(results) that takes a list of (name, status) tuples and returns the name of the first failure, or "" if there are none. Verify with a list that has one, and one that has none.
Exercise 4 — count the prints. Take Section 4's first_error and add print("checked:", line) before the if instead of after it. Predict how many checked: lines appear, then verify, and say what that tells you about when return takes effect.
Exercise 5 — None arrives. Write a function that prints a summary and returns nothing, then try to use its result in arithmetic. Record the error and explain which of the two channels was empty.
Exercise 6 — return a record. Write summarise(runtimes) returning a dictionary with count, total and longest. Verify on [120, 450, 90] and on [], and say what your function does for the empty case.
Exercise 7 — leave two loops. Rewrite Chapter 10.11 Section 6's flag-and-two-breaks program as a function using one return. Count the lines removed.
Exercise 8 — complete the header. For each description, write only the def line: (a) counts a given marker in given lines (b) decides pass or fail from an error count and a fatal count (c) formats a test name and a seed into a job id (d) returns a fixed separator.
11. Summary
- A function has two channels: values in through parameters, one value out through
return. - Everything the result depends on should arrive as an argument. Otherwise the same call can return different answers at different times.
- A complete header is the point — reading
def count(lines, marker):should tell you everything the function needs. - A genuinely constant module-level value is the narrow exception. The test is whether anything ever reassigns it.
- Passing the hidden input in usually makes the function more capable, not just safer.
returnends the call immediately — not just the block, not just the loop, but every enclosing loop too.- That makes
returnthe clean answer to leaving a nested search, replacing Chapter 10.11's flag and secondbreak. - A function may have several
returnstatements, one per outcome. - No
returnmeansNone, and so does a barereturn. - One value can be a collection, so a dictionary returns several named fields at once.
- A missing argument is a loud
TypeError; a missing return is silent.
The next chapter makes some of those arguments optional.
Where this fits
Part of the Python curriculum.
