The first organizing tool. You learn to write named functions with parameters, return values, and docstrings. The lab refactors your Lab 2 guess-the-number game so the same code is split into three named functions and the game itself reads like an outline.
Theme
A program of more than ~50 lines that is one long script becomes unreadable. Not all at once; gradually, as you add features. The way out is to break the program into named pieces called functions. A function is a chunk of code with a name, a defined input (parameters), and a defined output (return value). Once you have functions, the program's "main" code reads like a recipe: do this thing; then do this thing; then this.
This week's lecture is small in syntax (one new keyword: def) and large in habit (when to extract a function; how to name it; what a docstring should say). The lab takes Lab 2's guess-the-number game and refactors it: the random-number pick becomes a function, the guess loop becomes a function, the win/lose summary becomes a function, and the script's main code is six lines that call those three functions in sequence.
By the end of week 3 you can: write a function definition with def, distinguish parameters from arguments, distinguish return values from side effects, write a one-line docstring that explains what a function does without reading its body, and recognize when a function is "too big" (a heuristic: ~20 lines is the upper limit before extraction helps).
You will also encounter the scope rules. A variable defined inside a function is local to that function; the same name outside is a different variable. This is the source of the most common "but it worked when I tested it!" surprise in week 3.
Reading list (~1 hour)
- Matthes, Python Crash Course 2nd ed., Ch 8 ("Functions"). Matthes' chapter is the most beginner-friendly walkthrough of
def, parameters, return values, and the difference between positional and keyword arguments. - Sweigart, Automate the Boring Stuff with Python 2nd ed., Ch 3 ("Functions") at
https://automatetheboringstuff.com/2e/chapter3/. Free online. Sweigart's section on local vs global scope ("The Local and Global Scopes") is the clearest in print. - Allen B. Downey, Think Python 2nd ed., Ch 3 ("Functions") at
https://greenteapress.com/thinkpython2/html/thinkpython2004.html. Free online. Downey's framing of "fruitful" (returns a value) vs "void" (returns None) functions, in his §6.1, is a useful distinction the FND-102 lecture borrows. - PEP 257 (Docstring Conventions) at
https://peps.python.org/pep-0257/. Short read (~10 min). The "what should a docstring say" answer. FND-102 uses one-line docstrings for short functions; multi-line for anything with non-obvious arguments.
Lecture outline (~1.5 hours, 2 sessions of ~50 min)
Session 1: Defining and calling functions
Section 1.1: The shape of a function
defintroduces a function definition:def greet(name): print(f'hello, {name}')
- The line
def greet(name):is the signature.greetis the function name;nameis the parameter. - The indented block below is the body.
- The function is defined but not called until you invoke it:
greet('jamie'). - Definitions create the function object; they do not run the body. The body runs each time the function is called.
Section 1.2: Parameters vs arguments
- A parameter is the name in the function definition (
nameindef greet(name):). - An argument is the value you pass in when you call (
'jamie'ingreet('jamie')). - Multiple parameters are comma-separated:
def add(a, b): return a + b. - Call with positional arguments:
add(2, 3). Or keyword arguments:add(a=2, b=3). Or mix (positional must come first):add(2, b=3). - Default values let you make arguments optional:
def greet(name, greeting='hello'): print(f'{greeting}, {name}'). Nowgreet('jamie')works;greet('jamie', 'hi')also works. - A famous gotcha: do NOT use a mutable default value.
def f(x=[]): x.append(1); return xcreates a list ONCE at function-definition time; every call mutates the same list. Usedef f(x=None): x = x or []instead.
Section 1.3: Return values
returnends the function and hands a value back to the caller:def square(n): return n * n
- A function with no
return(or withreturnand no value) returnsNone.printreturnsNone; that is whyresult = print('hi')makesresultbeNone. - A function can have multiple
returnstatements: the first one reached wins.def absolute(n): if n < 0: return -n return n
- "Return early" vs "single exit point": both are legal styles. FND-102 prefers early-return because it reduces nesting; some style guides prefer single-exit. Pick a style per project and stay consistent.
Section 1.4: Side effects
- A function that returns a value but does not modify the outside world is pure.
square(n)is pure. - A function that modifies the outside world is impure; the modification is a side effect.
print(...)has a side effect (output to the terminal). Functions that write files, mutate global state, or modify their arguments all have side effects. - Side effects are not bad; they are how programs interact with the world. The discipline is to notice them and document them. A function called
compute_totalthat secretly writes a file is a future debugging headache.
Session 2: Scope and docstrings
Section 2.1: Local scope
- Variables defined inside a function are local to that function. They do not exist outside.
def f(): x = 5 print(x) # prints 5 f() print(x) # NameError: x is not defined
- This is intentional and good. Local scope means functions cannot accidentally mess with each other's variables.
- A function can read variables defined outside (in the enclosing module). It cannot assign to them without the
globalkeyword (which you should rarely use; see below).
Section 2.2: Reading vs writing
- Reading an enclosing variable:
greeting = 'hello' def greet(name): print(f'{greeting}, {name}') # reads `greeting` from outer scope; this works greet('jamie') # prints "hello, jamie"
- Writing to an enclosing variable (without
global):count = 0 def increment(): count += 1 # UnboundLocalError: `count` is treated as local because of the assignment increment() # crashes
- The fix:
def increment(): global count; count += 1. This is technically correct but a code smell. Better: passcountas a parameter andreturnthe new value. - The mental rule: "If a function reads a value, prefer to pass it as a parameter. If a function changes a value, prefer to return the new one."
Section 2.3: Docstrings
- The first string literal in a function body is a docstring. It is part of the function (accessible via
function.__doc__and viahelp(function)).def square(n): """Return the square of n.""" return n * n help(square) # prints the docstring
- A docstring is different from a comment: a comment is for the reader of the source; a docstring is for the user of the function.
help()displays docstrings; it does not display comments. - Convention (PEP 257):
- One-line: imperative voice.
"""Return the absolute value of n."""not"""Returns the absolute value."""or"""This returns the absolute value.""" - Multi-line: first line is a one-line summary; blank line; then more detail.
def parse_log(path): """Parse a log file and return a list of (timestamp, level, message) tuples. Raises FileNotFoundError if `path` does not exist. Lines that do not match the expected format are silently skipped. """
- One-line: imperative voice.
- A function without a docstring is acceptable for one-line helpers. Anything longer or anything called from multiple places should have one.
Section 2.4: When to extract a function
- Three signals it is time to extract:
- You repeated yourself. The same 4 lines appear in two places; extract into a function called once from each place.
- You wrote a comment explaining what the next 10 lines do. That comment is the function's name and docstring.
- The current function is more than ~20 lines. Not a hard rule; a sign the function is doing more than one thing.
- Counter-rule: do not extract just to extract. A function called from one place that is only 3 lines is sometimes harder to read than 3 inline lines.
Labs (~90 minutes)
Lab 3: Functional Refactor (labs/lab-3-functional-refactor.md)
- Goal: take your Lab 2 guess-the-number game and refactor it into named functions with docstrings. The script's main code should be ~6 lines long.
- Time: ~90 minutes
- Artifact:
lab-3-guess.pyin~/fnd-102/lab-3/, committed to Git
Independent practice (~4 hours)
-
Function-extraction drill (45 min). Take any tutorial Python script you can find online (Real Python, GeeksforGeeks, Sweigart's free book). Read 50 lines of a script. Identify three opportunities to extract a function. Write down (no code yet) what the function would be called, what its parameters would be, and what it would return.
-
Pure vs impure (30 min). Write a Python module containing exactly two functions:
read_temperature_csv(path): readstemps.csvand returns a list of floatsprint_temperature_summary(temps): prints min, max, mean
The first is impure (it reads a file). The second is also impure (it prints). Now refactor: write a third function
compute_summary(temps)that returns a dict{'min': ..., 'max': ..., 'mean': ...}and is pure. The print function calls it. The point: pure functions are easier to test. -
Default arguments (30 min). Write a function
greet(name, greeting='hello', punctuation='!')that returns the formatted string. Call it five ways: all defaults, override greeting, override punctuation, override both, all by keyword. Notice the readability difference between positional and keyword arguments. -
Scope exploration (30 min). In a
.pyfile, write this and predict the output:x = 10 def f(): x = 20 print(f'inside: x = {x}') def g(): print(f'inside g: x = {x}') f() g() print(f'outside: x = {x}')
Then change
fto useglobal xand re-predict. Then changegto assign toxand observe theUnboundLocalError. -
Docstring practice (30 min). Take three of your Lab 3 functions and rewrite the docstring three ways: too short ("does the thing"); just right (one imperative sentence); too long (a 5-line essay). Submit the just-right version. Notice how different from "code comments" a docstring is.
-
Read
help()on a stdlib function (15 min). In the REPL:import os; help(os.path.join). Notice the docstring format. Notice the parameter names. This is what a good docstring looks like in the wild. -
Optional stretch (60 min). Write a "calculator" function that takes an operator string (
'+','-','*','/') and two numbers, returns the result. Handle division by zero (returnNoneor raise an exception; pick one and document the choice in the docstring). Add amatchstatement (Python 3.10+) version and compare with theif/elifversion.
Reflection prompts (~30 minutes)
- Before this week, would you have extracted a function for a 4-line repeated block? Why or why not? Did your view change?
- Your Lab 2 was probably 30-50 lines. Your Lab 3 refactor is the same logic in three functions plus six lines of main code. Which version do you find easier to read? Which would you find easier to modify in a month?
- A pure function (no side effects, only returns a value) is easier to test. Your
pick_secret_number()from Lab 3 callsrandom.randint(...), which means it is not pure (it depends on the random state). What would you have to change to make it testable? (You will revisit this in week 13.) - Docstrings are part of the function; comments are not. Did you write any comments in Lab 3 that could have been docstrings instead? Move them.
- One thing from this week you want to know more about?
Tool journal (week 3)
def: define a functionreturn: hand a value back to the caller- Parameters with default values (
def f(x, y=0):) - Positional vs keyword arguments
- Docstrings:
"""..."""as the first line of a function body help(): print a function's docstringfunction.__doc__: access the docstring programmaticallyglobalkeyword: for the rare case you must write to an outer-scope variable
What comes next
Week 4 introduces the standard collection types: lists, dictionaries, tuples, sets. Your Lab 3 functions probably pass simple values (one number, one string) around. Real programs pass structured data: a list of guesses, a dictionary of player scores, a set of unique words from a file. Week 4's lab is a class-roster tool that reads a CSV and groups students by grade; exactly that pattern.