AmouAI Hub/Courses/Programming Fundamentals/Day 6
Functions I: Definition, Parameters, Return
Why functions exist, and the one distinction beginners collapse: returning a value is not the same as printing it.
By the end of today you can
- Say what a function buys you, in three specific things — not "reusability"
- Write a function with
def, parameters and areturnvalue - Explain why
returnandprintare not the same, and predict when you getNone - Trace the flow of control through a call and back, including code that never runs
- Use default and keyword arguments, and say when each earns its place
- Design a signature: name, parameters, return value
- Predict what a name means inside a function versus outside it
- Refactor
number_analyzer.pyinto functions without changing a byte of its output
▶Today's videos
Watch each video, then work the matching sections below. Watching alone will not do it.
def and the call/return flow · parameters versus arguments · return versus print, at length, with the mistake made on camera · where None comes from · code after return · local scope and UnboundLocalError.number_analyzer.py refactor, live, including the two wrong turns.1Why functions exist
Three specific things — and "reusability" is only one of them.
Yesterday you wrote about sixty lines as one straight run. It worked. But to change one line of it safely you had to hold the whole thing in your head — and sixty lines is roughly where that stops being possible. Not because you are not clever enough. Because nobody can.
The problem
celsius = 21.5
fahrenheit = celsius * 9 / 5 + 32
print(f"{celsius}C = {fahrenheit}F")
celsius = 36.6
fahrenheit = celsius * 9 / 5 + 32
print(f"{celsius}C = {fahrenheit}F")
celsius = 100
fahrenheit = celsius * 9 / 5 + 32
print(f"{celsius}C = {fahrenheit}F")The formula appears three times. If it is wrong, it is wrong three times — and you will fix two of them. That is not carelessness; it is what duplicated logic does to people.
The fix
def to_fahrenheit(celsius):
return celsius * 9 / 5 + 32
for temperature in (21.5, 36.6, 100):
print(f"{temperature}C = {to_fahrenheit(temperature)}F")97.88000000000001 is the Day 1 float lesson arriving again: 36.6 cannot be stored exactly in binary. But now it arrives inside a function, so you can call to_fahrenheit(36.6) on its own and see it, without running anything else. That is job 3, below.
| Job | What it means | Notes |
|---|---|---|
| 1 · Remove repetition | One place to write it, fix it, improve it | The obvious one |
| 2 · Name an idea | An expression becomes a sentence | The one people undersell |
| 3 · Isolate a piece of work | Test it, break it, replace it on its own | Saves the most hours |
On job 2: celsius * 9 / 5 + 32 is an expression you must decode. to_fahrenheit(celsius) is a sentence you read at speed. In a real codebase a large fraction of functions are called exactly once — and they still earn their place, for exactly this reason.
Day 5 taught you to binary-search a program with labelled prints. Functions pre-cut the program along exactly the lines you would have searched anyway. Good decomposition is debugging you did before you needed it.
2def, parameters, return
The mechanics — and where control actually goes.
It creates the function and puts it in a name, exactly the way = puts a value in a name. A file full of def statements and no calls does nothing at all — no output, no error. This is the most common first mistake.
Parameter or argument?
| Term | Where it lives | Example |
|---|---|---|
| Parameter | In the def line — a name waiting for a value | celsius |
| Argument | At the call site — the actual value you send | 21.5 |
It rarely matters in conversation, but it makes error messages readable: "missing 1 required positional argument: 'name'" means a parameter never received an argument — so the mistake is at the call site, not in the body.
return stops the function
Which is what makes Day 3's guard clause work inside a function:
def safe_divide(top, bottom):
if bottom == 0:
return 0.0 # leave early — the rest never runs
return top / bottom
print(safe_divide(10, 4))
print(safe_divide(10, 0))No else is needed. If bottom were zero, we would not be here.
3return is not print
This is the section. If you take one thing from today, take this.
print(x) | return x | |
|---|---|---|
| Sends the value to | the screen | the code that called it |
| Audience | a human | the program |
| Can the program use it? | No — it is gone | Yes. That is the point. |
| The call evaluates to | None | x |
| Useful with nobody watching? | No | Yes |
The test that settles it: can you use the answer?
Whenever you meet NoneType in a TypeError, the first thing to check is whether a function you wrote prints where it should return. You will hit this more than any other error this week.
Where None comes from
A function that never executes a return hands back None. Python does it silently. And print itself returns None:
def no_return():
total = 1 + 1
print(no_return())
result = print("hi")
print(result)Which explains the first demonstration exactly. add_print ends with a print call — but ending with a call is not returning its result. Nothing was returned, so None came back.
A function should either compute a value or talk to the user. Rarely both. If printing is genuinely the job — show_menu, print_receipt — then print, and let the name say so.
4Defaults and keyword arguments
Two conveniences — and one of them is really about readability.
def greet(name, greeting="Hello", punctuation="!"):
return f"{greeting}, {name}{punctuation}"
print(greet("Amin"))
print(greet("Amin", "Welcome"))
print(greet("Amin", punctuation="?"))
print(greet(punctuation="?", name="Amin"))That last call passes both arguments out of order and still works, because both are named. Position only matters for arguments that are not.
| Rule | Break it and you get |
|---|---|
| Defaults must come last | SyntaxError: non-default argument follows default argument |
| Positional before keyword | SyntaxError: positional argument follows keyword argument |
| Cannot fill a parameter twice | TypeError: greet() got multiple values for argument 'name' |
The first two are SyntaxErrors — caught before anything runs. The third is a TypeError, at call time. That is Day 5's syntax-versus-runtime distinction turning up in a new place.
What keyword arguments are really for
When an argument is a bare True, False, or a number with no obvious meaning, name it. Eight characters, and the next reader never leaves the line. Values that speak for themselves — a name, a price, a filename — can stay positional.
The call errors you will actually hit
Default values are evaluated once, when the def line runs — not on each call. With numbers and strings this never bites. With lists it bites hard, and we come back to it on Day 8.
5Designing a signature
The only part of your function that other people read.
| Kind of function | Name it like | Examples |
|---|---|---|
| Returns a value | a noun, or _of | mean_of, full_name, total_price |
| Returns True / False | a question | is_number, has_expired, can_afford |
| Performs an action | a verb | save_report, show_menu, send_email |
If the honest name needs an and — validate_and_save, read_and_print — you have two functions wearing one coat. Split them. Notice how good a deal that is: the name told you the design was wrong before you wrote a line of the body.
Take what you need, and no more
Three parameters is comfortable. Four is a smell. Six means an idea is missing from your design.
Local names are local
The trap that follows
count = 0
def bump():
count = count + 1
bump()Python scans the whole body first. Because count is assigned somewhere in bump, it is local throughout — including on the right-hand side, where it has no value yet.
There is a global keyword that makes this work. It is almost never the right answer, because a function that modifies things outside itself is one you cannot reason about, test, or move. The fix is four words:
def bump(count):
return count + 1
count = bump(count)Take what you need, hand back what you made, touch nothing else. This is the single most valuable habit in today's material.
6Refactoring Mini-Project 1
Same program. Same output. Ten functions.
The output must not change. Not one byte. A refactor changes structure, never behaviour. If the report comes out different, you did not refactor — you broke something.
python3 number_analyzer_old.py > before.txt
python3 number_analyzer.py > after.txt
diff before.txt after.txt # silence means successRun that after every function you extract — not once at the end. Extract ten and then find a difference, and you have ten places to look. Check after each and you always have exactly one.
Finding the seams
| Stretch of yesterday's program | Becomes |
|---|---|
| The validity check | is_number(text) |
The while True input loop | collect_numbers() |
count += 1 | count_of(numbers_text) |
total += value | total_of(numbers_text) |
| The min / max tracking | smallest_of / largest_of |
| The even / odd branch | count_whole_with_remainder(text, r) |
| The second pass | count_above(numbers_text, threshold) |
| The block of prints | format_report(text) — returns the text |
| Everything, in order | main() |
The isolation payoff
Yesterday is_number was a variable holding True or False, buried inside a loop. Today it is a function — which makes this possible:
for text in ["12", "-3.5", "0", ".", "", "abc", "1.2.3", "--5", "+5"]:
print(f"is_number({text!r:>8}) -> {is_number(text)}")Nine test cases, no typing at a prompt, one second. And look at the last line: '+5' is rejected. A leading plus is a valid way to write a positive number, and this validator refuses it. You did not have to be clever to find that — you asked the function nine questions and read the answers. Finding out what your code actually does is most of what testing is.
The shape of the result
Three things to notice. collect_numbers returns from inside the loop — return numbers_text does the job break did yesterday, and does it better. format_report returns a string; it does not print. main() has a guard clause, so everything below it can assume there is at least one number.
The traceback now tells a story
And the cost, honestly
format_report calls four counters, and every one splits the same string again. The program now walks the same data five times where the old one walked it once. That is genuinely worse along that one dimension, and you should notice it rather than be told later.
When they arrive, numbers_text becomes numbers and every one of these functions gets shorter. Watch for this: the signatures barely change. count_above still takes the numbers and a threshold and still returns a count. The bodies change; the boundaries survive. That is what a good interface is — and you drew those lines today.
>_Python playground
A real Python interpreter running inside your browser. Nothing is installed, nothing is uploaded, nothing can break.
input(), comma separated →
✓Exercise set
Checked automatically the moment you submit. Work top to bottom — each one assumes the last. Your answers are saved in this browser.
Day 7 — Functions II: Scope, Recursion, Composition
Where names live, why side effects hurt, and recursion that reads better than a loop.