Day 6 / 25 Functions I: Definition, Parameters, Return 0/0 exercises Exercises ↓

AmouAI Hub/Courses/Programming Fundamentals/Day 6

Week 2 · Functions & Data Structures · 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.

Study time
4 hours
Reading
Think Python, Ch. 3 & 6
Focus
def · parameters · return

By the end of today you can

  1. Say what a function buys you, in three specific things — not "reusability"
  2. Write a function with def, parameters and a return value
  3. Explain why return and print are not the same, and predict when you get None
  4. Trace the flow of control through a call and back, including code that never runs
  5. Use default and keyword arguments, and say when each earns its place
  6. Design a signature: name, parameters, return value
  7. Predict what a name means inside a function versus outside it
  8. Refactor number_analyzer.py into 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.

6A
Functions & the Return/Print Distinction (120 min)
Why functions exist · 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.
6B
Signatures & the Mini-Project Refactor (120 min)
Default arguments · keyword arguments and call-site readability · designing a signature · one job per function · separating compute from talk-to-the-user · the full 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

before.py
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

after.py
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")
21.5C = 70.7F 36.6C = 97.88000000000001F 100C = 212.0F
That long number is not a bug

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.

JobWhat it meansNotes
1 · Remove repetitionOne place to write it, fix it, improve itThe obvious one
2 · Name an ideaAn expression becomes a sentenceThe one people undersell
3 · Isolate a piece of workTest it, break it, replace it on its ownSaves 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.

Job 3 is yesterday's lesson, paid forward

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.

def does not run anything

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?

TermWhere it livesExample
ParameterIn the def line — a name waiting for a valuecelsius
ArgumentAt the call site — the actual value you send21.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:

guard.py
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))
2.5 0.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 tothe screenthe code that called it
Audiencea humanthe program
Can the program use it?No — it is goneYes. That is the point.
The call evaluates toNonex
Useful with nobody watching?NoYes

The test that settles it: can you use the answer?

NoneType in a TypeError is the signature of this mistake

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:

none.py
def no_return():
    total = 1 + 1


print(no_return())

result = print("hi")
print(result)
None hi None

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.

The rule

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.

defaults.py
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"))
Hello, Amin! Welcome, Amin! Hello, Amin? Hello, 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.

RuleBreak it and you get
Defaults must come lastSyntaxError: non-default argument follows default argument
Positional before keywordSyntaxError: positional argument follows keyword argument
Cannot fill a parameter twiceTypeError: 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

Flag forward

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 functionName it likeExamples
Returns a valuea noun, or _ofmean_of, full_name, total_price
Returns True / Falsea questionis_number, has_expired, can_afford
Performs an actiona verbsave_report, show_menu, send_email
The "and" test

If the honest name needs an andvalidate_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

unbound.py
count = 0

def bump():
    count = count + 1

bump()
UnboundLocalError: local variable 'count' referenced before assignment

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:

the_fix.py
def bump(count):
    return count + 1

count = bump(count)
Arguments in, values out

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 rule, and it is not negotiable

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.

terminal
python3 number_analyzer_old.py > before.txt
python3 number_analyzer.py     > after.txt
diff before.txt after.txt        # silence means success

Run 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 programBecomes
The validity checkis_number(text)
The while True input loopcollect_numbers()
count += 1count_of(numbers_text)
total += valuetotal_of(numbers_text)
The min / max trackingsmallest_of / largest_of
The even / odd branchcount_whole_with_remainder(text, r)
The second passcount_above(numbers_text, threshold)
The block of printsformat_report(text)returns the text
Everything, in ordermain()

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:

test_it.py
for text in ["12", "-3.5", "0", ".", "", "abc", "1.2.3", "--5", "+5"]:
    print(f"is_number({text!r:>8}) -> {is_number(text)}")
is_number( '12') -> True is_number( '-3.5') -> True is_number( '0') -> True is_number( '.') -> False is_number( '') -> False is_number( 'abc') -> False is_number( '1.2.3') -> False is_number( '--5') -> False is_number( '+5') -> False
And it tells you the truth

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 loopreturn 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.

Which is exactly why lists exist — tomorrow

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.

scratch.pypython not loaded
Values for input(), comma separated →
Output appears here. The first run takes a few seconds while Python loads.

Exercise set

Checked automatically the moment you submit. Work top to bottom — each one assumes the last. Your answers are saved in this browser.

All Warm-up Core Challenge Reset day

Day 7 — Functions II: Scope, Recursion, Composition

Where names live, why side effects hurt, and recursion that reads better than a loop.

Continue →