Lecture 3 · Function Interfaces and Reusable Analysis
Block 1 · Week 2 | Chapters 4–5 and the Grade Calculator
Function interfaces and reusable analysis¶
Arguments, data records, and small functions that another person can call¶
Lecture 2 established the basic function contract: named inputs, validation,
and return. Today we continue that work. We will make calls readable, pass
structured observations without losing their meaning, and decide when a
function should accept a flexible number of inputs.
The through-line¶
Scientific question
Named quantities and units
A readable function call
A small, stated interface
Returned result and test
Reusable analysis
The syntax matters because it makes the relationship among values visible. One computation should be understandable at its definition and at each call site.
Today’s question¶
How do we design a function call so its inputs, defaults, and returned value remain clear when someone else uses it later?
By the end of class, you should be able to:
distinguish positional, keyword, default, and keyword-only arguments;
use
*argsand**kwargsdeliberately rather than as catch-all syntax;unpack a
tupleordictinto a function call without hiding meaning;choose whether a helper returns a value, a collection, or an error; and
use small functions to implement and test a published rule.
I · Read a call as part of the function’s contract¶
Question¶
When is the order of two correct numbers still too ambiguous for a reader?
You will leave this section able to¶
name the difference between a parameter and an argument;
use positional and keyword calls for their appropriate roles;
explain what a default says about a common case; and
require a consequential option to be named.
Through-line focus: named quantities → readable call.
Parameters are names in a definition; arguments are values at a call¶
def phase_fraction(time_days, period_days, *, epoch_days=0.0):
"""Return phase in [0, 1) for time, period, and epoch in days."""
if period_days <= 0:
raise ValueError("period_days must be positive")
return ((time_days - epoch_days) / period_days) % 1.0| In the definition | In this call | Meaning |
|---|---|---|
time_days | 3.5 | observation time in days |
period_days | 2.0 | repeating period in days |
epoch_days | epoch_days=0.5 | declared phase reference in days |
The parameters are local names. The arguments are particular values supplied by one caller. A function body should not need to know where those values came from.
For this toy example, phase means position within one repeating cycle:
(3.5 days - 0.5 day epoch) / 2.0 day period = 1.5 cycles
1.5 % 1.0 = 0.5 phaseThe % operator keeps only the fractional cycle. We use this small
calculation to practice function interfaces, not to infer a physical period.
Positional arguments are good when order is conventional and short¶
phase_fraction(3.5, 2.0)Use positional arguments when their roles are familiar and short:
there are only one or two required inputs;
their order is conventional in this domain; and
swapping them would be obviously wrong or caught by the contract.
math.sqrt(9.0) is readable positionally because the operation has one input.
phase_fraction(3.5, 2.0) is tolerable in a short local example; a less familiar
call deserves more help.
Keywords preserve the role of each value¶
phase = phase_fraction(
time_days=3.5,
period_days=2.0,
epoch_days=0.5,
)The numerical result is the same as the preceding positional call. The inspection burden is not: the units and roles are visible where the values are chosen.
Use keywords when values have the same type or unit but different scientific roles, when a call has more than two inputs, or when an option changes the interpretation of the result. Keywords do not compensate for vague parameter names; choose names that say what the value represents.
Predict · which calls describe the same phase?¶
Before running, identify the two calls that must agree. Then change the epoch: which call makes the scientific convention easiest to find six weeks from now?
Worked result: the first two calls agree because each uses epoch_days=0.0.
The third changes only when the named epoch differs from zero.
A default is a stated common case, not a missing decision¶
The signature def phase_fraction(time_days, period_days, *, epoch_days=0.0):
says: “unless the caller declares a different reference, phase zero is at time
zero.” The validated body is on the preceding slide. That can be a sound
convention for a toy model or a local analysis. It is not suitable if the
reference time is unknown or scientifically essential.
| Good use of a default | Poor use of a default |
|---|---|
grid_points=200 for a documented display grid | period_days=1.0 when the period is unknown |
show_grid=True for a plotting preference | flux_unit="normalized" when units are not known |
epoch_days=0.0 in a declared toy convention | a placeholder that silently changes interpretation |
Rule: a default belongs in the documented contract.
Keyword-only parameters protect consequential options¶
def phase_fraction(time_days, period_days, *, epoch_days=0.0):
"""Return phase in [0, 1) for a declared epoch in days."""
if period_days <= 0:
raise ValueError("period_days must be positive")
return ((time_days - epoch_days) / period_days) % 1.0
phase_fraction(3.5, 2.0, epoch_days=0.5) # clear
try:
phase_fraction(3.5, 2.0, 0.5)
except TypeError as error:
print(error)The * makes every following parameter keyword-only. Use it when an optional
value could be mistaken for another number with the same units. Required time
and period remain positional-or-keyword arguments.
phase_fraction(3.5, 2.0, 0.5) raises TypeError; the reference convention
must be named.
Reference · ordinary function parameters¶
| Signature form | Caller supplies it as… | Use when… |
|---|---|---|
f(x) | a required positional or keyword argument | one essential, unambiguous input |
f(x, y) | two required arguments | order is short and documented |
f(x, option=True) | an optional positional or keyword argument | the default is a stated common case |
f(x, *, option=True) | a keyword argument only | the option changes interpretation or is easy to swap |
f(*, unit) | a required keyword argument | a named choice must never be omitted |
Rule: the function signature is part of the documentation. Read it before you call an unfamiliar function and design it before others call yours.
II · Flexible calls need a narrow purpose¶
Question¶
What do *args and **kwargs collect—and when do they make an interface less
clear instead of more useful?
You will leave this section able to¶
collect a variable number of same-role positional values with
*args;collect named options with
**kwargsonly at a deliberate boundary;unpack a
tuplewith*and amappingwith**; andreject unknown options rather than silently ignoring them.
Through-line focus: related inputs → a deliberate interface.
*args gathers extra positional arguments into one tuple¶
def mean_flux(*flux_values):
"""Return the mean of one or more dimensionless flux values."""
if not flux_values:
raise ValueError("supply at least one flux value")
return sum(flux_values) / len(flux_values)
print(mean_flux(1.000, 0.993, 0.989, 0.975))Inside this function, flux_values is the tuple
(1.000, 0.993, 0.989, 0.975). *args is appropriate because every collected
value has exactly the same role and units.
It is a poor choice for unrelated inputs such as time, flux, period, and an instrument name. Those need their own parameter names or a documented record.
Known options should be explicit keyword-only parameters¶
def describe_series(values, *, label="series", unit=""):
return f"{label}: {len(values)} values {unit}".strip()
print(describe_series([1.0, 0.99], label="normalized flux", unit=""))The two supported options are known in advance, so the signature names them. Python now rejects a misspelling automatically. Use this pattern for the ordinary public interface of a small course function.
**kwargs is useful at a documented forwarding boundary¶
def plot_series(ax, time_days, brightness, **plot_options):
allowed = {"label", "color", "marker", "linestyle"}
unknown = set(plot_options) - allowed
if unknown:
raise TypeError(f"unknown plot option: {sorted(unknown)}")
return ax.plot(time_days, brightness, **plot_options)plot_options is a dict inside this function. Use **kwargs to preserve a
small, documented set of named options at a boundary; do not hide the ordinary
input contract.
Run this complete check:
class FakeAxes:
def plot(self, time_days, brightness, **options):
return options
ax = FakeAxes()
time_days = [0.0, 1.0]
brightness = [1.000, 0.990]
try:
plot_series(ax, time_days, brightness, lable="synthetic observations")
except TypeError as error:
print(error)The misspelled lable raises a TypeError before FakeAxes.plot is called. A
wrapper that silently accepts it
would make a later figure harder to trust.
| Interface choice | What it communicates |
|---|---|
| explicit parameters | the supported inputs are known in advance |
*args | any number of values with one shared role |
**kwargs with checking | named options pass through one defined boundary |
unrestricted **kwargs | the supported interface is unclear |
Rule: flexible calls need an explicit contract.
Run · pack and inspect a variable-length measurement set¶
Change one reading. Why is mean_flux(*measurements) not the same syntax as
mean_flux(measurements)? What type would the latter supply as its one argument?
Worked result: *measurements unpacks the tuple into three arguments;
without *, the function receives one tuple and cannot add it to a float.
Unpacking lets a record meet a matching signature¶
observation = {
"time_days": 3.5,
"period_days": 2.0,
"epoch_days": 0.5,
}
phase = phase_fraction(**observation)Using the phase_fraction definition above, **observation supplies each
key-value pair as a named argument. Its keys must match the function’s
documented parameter names. Validation and unit handling remain part of the
function contract.
For a short ordered pair, tuple unpacking is also useful:
time_days, flux = (3.5, 0.987)The left-hand side names the roles; the tuple preserves their order.
Select the fields a helper promises to use¶
observation = {
"target_id": "demo-17",
"time_days": 3.5,
"period_days": 2.0,
"epoch_days": 0.5,
"filter_name": "r",
}
phase_inputs = {
name: observation[name]
for name in ("time_days", "period_days", "epoch_days")
}
phase = phase_fraction(**phase_inputs)Do not send an entire record to **kwargs and hope the function finds the
right fields. Select the fields the function promises to use. That selection is
part of the analysis logic and should be easy to inspect.
Reference · packing and unpacking¶
| Syntax | At a definition | At a call |
|---|---|---|
*values | collects extra positional arguments into a tuple | unpacks an iterable into positional arguments |
**options | collects extra keyword arguments into a dict | unpacks a mapping into keyword arguments |
a, b = pair | — | unpacks an ordered pair into named roles |
Before using * or **, say the sentence in plain language: “these are many
values of the same kind,” or “these dict keys match this function’s
documented parameters.” If you cannot say that sentence, choose a clearer
representation.
III · Return values should support the next question¶
Question¶
How does a small function hand an analysis result back without mutating the input record or hiding a failure?
You will leave this section able to¶
choose between returning one value, a
tupleof values, or a new collection;distinguish a returned result from printed display text;
avoid changing an input collection unexpectedly; and
test an interface’s ordinary, boundary, and invalid cases.
Through-line focus: calculation → stated result.
return a result; print it at the human-facing boundary¶
def centered_fluxes(flux_values):
"""Return a new list centered on the mean flux."""
if not flux_values:
raise ValueError("flux_values must not be empty")
mean_flux_value = sum(flux_values) / len(flux_values)
return [flux - mean_flux_value for flux in flux_values]
raw_fluxes = [1.000, 0.993, 0.989]
residual_fluxes = centered_fluxes(raw_fluxes)
print("residuals:", residual_fluxes)centered_fluxes returns a value that later code can plot, compare, or test.
The print belongs outside the calculation because printing is a display
choice, not the mathematical result.
Returning a new collection preserves the raw record¶
def centered_fluxes(flux_values):
if not flux_values:
raise ValueError("flux_values must not be empty")
mean_flux_value = sum(flux_values) / len(flux_values)
return [flux - mean_flux_value for flux in flux_values]
raw_fluxes = [1.000, 0.993, 0.989]
residual_fluxes = centered_fluxes(raw_fluxes)
print("raw:", raw_fluxes)
print("residuals:", residual_fluxes)The function reads the input list but does not mutate it with
list.append(), list.remove(), list.sort(), or item assignment. A separate
returned list makes it possible to check the raw measurements later. If a
function does change an input in place, its name and documentation should say
so clearly.
Test the interface, not only its arithmetic¶
import math
assert phase_fraction(0.0, 2.0) == 0.0 # boundary
assert math.isclose(
phase_fraction(3.5, 2.0), 0.75, rel_tol=0.0, abs_tol=1e-12
) # ordinary
try:
phase_fraction(3.5, 0.0)
except ValueError:
pass
else:
raise AssertionError("nonpositive period was accepted")With phase_fraction defined above, an interface test checks the behavior the
caller relies on: a valid output for a stated input and a clear failure outside
the stated domain.
Run · change a raw series without changing the raw series¶
Predict the sign of the third residual before changing the slider. Why should the residuals sum to approximately zero, rather than exactly zero for every floating-point input?
Worked result: the return value is a new list; raw_fluxes remains the
three original readings. Floating-point arithmetic can leave a tiny roundoff.
Reference · return values and mutation¶
| Design choice | Use it when… | Caution |
|---|---|---|
return one value | one quantity answers the next question | document its units and domain |
return a tuple | a few results always travel together | unpack immediately into named roles |
return a dict | results have several named roles | make keys part of the contract |
return a new list | derived values should not alter raw inputs | state the ordering and length |
| mutate an input | in-place change is the explicit purpose | name and document the mutation |
raise an exception | the request violates the contract | explain which condition failed |
Rule: a caller should be able to tell whether a function returns, mutates, or refuses a request without reading every line of its body.
IV · A small policy program should keep its jobs separate¶
Question¶
How can a 30-minute grade-calculator activity exercise function interfaces without becoming an official grade record?
You will leave this section able to¶
decompose a calculation and a classification policy into separate helpers;
select direct boundary tests for the policy; and
distinguish a practice result from an authoritative course grade.
Through-line focus: published rule → small, testable program.
The grade-calculator lab is a practice policy, not a grade system¶
For the COMP 521 Course-Grade Calculator Lab, you will work only with the supplied fictional record. Canvas remains the authoritative course record.
Keep these jobs separate:
letter grade → grade points
component grades → component mean
component means + weights → unrounded course average
unrounded average → final course letterEach arrow is a small function with a different contract. This makes a wrong letter boundary easier to locate than it would be in one long cell.
A direct boundary test is better than a manufactured scenario¶
assert final_course_letter(3.80) == "A"
assert final_course_letter(3.79) == "A-"After defining final_course_letter in the lab, the point of these tests is
the branch boundary, not the construction of a
fictional record that happens to average to 3.80. Test the policy at its own
interface.
Use the full 30 minutes for the activity:
read the contract and supplied test table;
implement one small helper at a time;
run the direct policy tests plus one invalid-input test;
restart and run the notebook top to bottom; and
write one sentence explaining a failed test or a checked boundary.
Reference · readings and documentation¶
| Use this for… | Read |
|---|---|
| lists, dictionaries, tuples, and mutation | Chapter 4: Data Structures |
| definitions, arguments, scope, and modules | Chapter 5: Functions & Modules |
| Python parameter rules and call syntax | Python: defining functions |
| Python function calls and unpacking | Python: more on defining functions |
| the practice activity | COMP 521 Course-Grade Calculator Lab |
Read documentation at the point of use. A signature tells you how to call a function; its documentation tells you what that call means.
Exit check¶
For the function below, label each part of the call and explain one design choice.
phase = phase_fraction(5.0, 2.0, epoch_days=1.0)Which values are positional arguments? Which is keyword-only?
Repair this call so it is valid:
phase_fraction(5.0, 2.0, 1.0).A record has keys
time_days,period_days, andepoch_days. Which operator supplies it tophase_fractionby name?In one sentence: when would you choose
*argsinstead of explicit parameters, and when would you use**kwargs?
Bring one question about arrays, shapes, or plotting to Lecture 4.
Before Lecture 4¶
Read Chapter 7: NumPy Fundamentals through 7.4 Indexing and Boolean Masks, and Chapter 8: Matplotlib Fundamentals through the first labeled figure.
Be ready to explain:
why a NumPy array is not merely a faster list;
what one Boolean value in a mask corresponds to; and
what an axis label must tell a reader.
Lecture 4 uses a small, synthetic stellar-variability example—not Project 1—to practice the array and figure habits that later work will need.