def estimate_tokens(words):
return words * 4
print(estimate_tokens(20))80Read this as a study guide instead
Named inputs to outputs; the unit everything else is built from.
A notebook-style walk through the idea — every output shown is the real result of the code above it.
Define a function that returns a value, give a parameter a default, and return more than one value at once.
Real output Every Out block below was produced by running the code above it. You can copy these cells into your own python3 and run them top to bottom to see the same numbers.
In the pipeline, a function names a step you run for every request. You def it once, then call it wherever the step is needed — the call runs the body and hands back what it returns.
def estimate_tokens(words):
return words * 4
print(estimate_tokens(20))80A parameter can carry a default, so most calls use one attempt. Naming the argument at the call site (retries=3) makes which value is the retry count obvious.
def call_tool(name, retries=1):
return name + " x" + str(retries)
print(call_tool("search"))
print(call_tool("search", retries=3))search x1
search x3A function can hand back more than one value at once — here the fastest and slowest step latency across a run. The caller unpacks the pair in one line.
def step_latency(times):
return min(times), max(times)
fastest, slowest = step_latency([120, 90, 210, 80])
print(fastest, slowest)80 210These are the exact fragments the model serves — also available as an ordered study guide.
A function is a named piece of work: you describe how to turn some inputs into an output once, give it a name, and from then on you run it by name. The description lives in one place; every use of it points back there. That single move — write it once, call it many times — is why functions are the unit almost everything larger is built from.
The shape is always the same. A function takes named inputs, does something with them, and hands back a result. Nothing about the caller has to know how the work is done, only what goes in and what comes out like a recipe card: the same named steps turn the same ingredients into the same dish, and whoever cooks it only needs the card, not the reasoning behind it. This is the same contract a Python built-in like len offers: you pass it a list, you get back a count, and how it counts is not your problem.
The payoff is leverage. A calculation you would otherwise retype — and get subtly wrong the third time — becomes one tested definition you call from everywhere. Fix it once and every caller is fixed. The rest of this concept is just the mechanics of writing that definition and getting values in and out of it cleanly.
You define a function with def, a name, a parenthesised list of parameters, and a colon; the indented block beneath is the body. Writing def square(n): does not run anything — it binds the name square to a function object and moves on. The body waits, unused, until something calls it.
A call is the name followed by parentheses holding the arguments: square(8). At that point control leaves the calling line, jumps into the body with n set to 8, runs until it hits a return, and comes straight back to where it left off, carrying the returned value. return n * n sends 64 back, and square(8) becomes 64 in the expression that asked for it.
The distinction worth holding onto is between the parameter and the argument: n is the parameter, the placeholder named in the definition, and 8 is the argument, the actual value supplied at the call. One definition with the parameter n serves every argument you will ever pass it. Reaching the end of the body with no return is legal too — the function simply hands back None, Python's stand-in for no useful value.
Arguments reach parameters two ways. By position, power(5, 3) fills the parameters left to right — base gets 5, exp gets 3. By keyword, power(5, exp=3) names the target explicitly, so the value lands on exp no matter where it sits. Positional is compact; keyword is self-documenting, and it is the honest choice the moment a bare value at a call site would leave a reader guessing what it means.
A parameter can also carry a default, written in the definition as exp=2. A default makes that argument optional: power(5) runs with exp at 2, while power(5, exp=3) overrides it. Defaults let one function serve the common case with no ceremony and the special case with one extra argument, instead of splitting into two near-identical functions.
Two rules keep this unambiguous. Parameters with defaults come after those without, so Python can always tell which positional argument fills which slot. And a given parameter is filled once per call — supplying it both by position and by keyword is an error, not a silent last-wins. The grammar is deliberately strict here because a mis-slotted argument is exactly the kind of bug that runs without complaint and returns the wrong answer.
return hands a value back to the caller; print writes text to the screen. They are easy to confuse because both seem to "show" a result, but only one lets the result travel. A function that prints its answer and returns None has thrown the answer away — you can read it, but no other code can use it. A function that returns its answer can be stored, passed on, or fed straight into the next call. In a notebook the difference hides, because the last value in a cell gets displayed automatically; in a real program, an un-returned value is simply gone.
Reaching a return also ends the function immediately. Any lines after the one that runs are skipped, which is what makes an early return a clean way to handle a special case up front and leave the rest of the body for the normal path.
A function can hand back several values at once by returning them separated by commas: return min(numbers), max(numbers). That packs them into a single tuple, and the caller can unpack it in one line — low, high = minmax([4, 9, 2, 7]) — binding each returned value to its own name. It reads as "give me both", and it saves inventing a throwaway container just to carry two answers across the boundary.
Names created inside a function are local: they exist while the call runs and vanish when it returns. A variable you assign in the body, and the parameters themselves, live in a private workspace that belongs to that one call like a labeled box a worker fills only for the job in front of them and empties the moment the job is done, so the next job starts with a fresh one. Two calls to the same function never see each other's locals, and code outside the function cannot reach in and read them at all. When the function returns, that workspace is discarded along with everything in it except the value handed back.
This isolation is the quiet reason functions compose without surprises. Because a function's inner names cannot leak out and outside names cannot be clobbered from within, you can call one anywhere without auditing what it does to the rest of your variables. The boundary is the value that goes in and the value that comes out; nothing else crosses.
A function can still read a name defined outside it when that name is not assigned locally, which is how it uses things like an imported helper. But assigning to a name inside the body makes that name local for the whole call, shadowing any outer one. The habit that keeps this painless is to pass what a function needs in as arguments and send what it produces back with return, rather than reaching out to variables defined elsewhere — a function whose answer depends only on its arguments is one you can call, test, and trust in isolation.
Once you can name a computation and call it, the shape of every larger thing you build is the same move repeated. A program becomes a handful of well-named functions calling one another, each small enough to read in one sitting and named well enough that the call site explains itself. The alternative — one long block where every step is spelled out inline — works until it has to change, and then every change is a hunt.
The leverage compounds the moment work repeats or has to be trusted. A calculation wrapped in a function is written once and called from ten places, so a fix lands in all ten at once. It is also the natural thing to check in isolation: because a function that depends only on its arguments has one job and one answer, you can pin that answer down and know later edits have not broken it. The concepts that come next lean directly on this — bundling functions with the data they act on, and pointing automated checks at them.
This is also where a project stops being a script and starts being something you can extend. A racing robot's control loop, a request handler in a small web service, a step in an agent's pipeline — each is, underneath, functions passing values across clean boundaries. Learn to draw those boundaries well and the rest of the build has somewhere solid to stand.