We closed module 3 with a sentence: "you know how to build the machinery of a program, and now it is time to learn how to organise it". This is that moment. EasyTask v0.6 works — Marta can register a task, look it up, change its priority and complete it without relaunching the program — but the file easytask.py has turned into a wall of almost a hundred consecutive lines where if not has_task: appears three times and four validation loops repeat themselves almost word for word.
That wall is not an aesthetic problem but a practical one: when something breaks, you do not know where to look; when you want to change the priority error message, you have to remember to change it in both places where it is written; and when you want to add an option to the menu, you have to fight your way through the code of the previous five.
The tool that solves all of that is called a function. It is probably the most profitable idea in the whole course: the one that turns an unmanageable script into a program you can read, test and extend piece by piece. In this lesson you will learn what a function is, how it is defined, how it is called and what exactly happens when you call it.
Contents
- The problem: repeated code with no name
- What a function is
- Syntax:
def, indented body and call - Defining and calling are not the same thing
- Order of definition and execution: the
NameError - The flow of a call, step by step
- Procedures and functions that compute:
None - Docstring and naming conventions
- EasyTask: the first three functions
- Common mistakes and tips
- Exercises
- Conclusion
- The problem: repeated code with no name
Look at these two fragments, taken exactly as they are from EasyTask v0.6. One lives in option 1 and the other in option 3:
# In option 1 (register task)
priority = input("Priority : ").strip().lower()
while priority not in PRIORITIES:
priority = input("Not valid. Priority: ").strip().lower()
# In option 3 (change priority)
new_priority = input("New priority (high/medium/low): ").strip().lower()
while new_priority not in PRIORITIES:
new_priority = input("Not valid. Priority: ").strip().lower()They are the same algorithm written twice, and that brings three unpleasant consequences:
- Double maintenance. If tomorrow the studio adds the priority
critical, you have to touch both places. And the day you only touch one, you will have a program that behaves differently depending on which door you come in through: a particularly hard bug to find. - Noise. When you read option 1 you do not see "the priority is requested": you see three lines of machinery with
input,strip,lowerand awhile. The what is buried under the how. - No way to test in pieces. You cannot check "does my priority reading work properly?" without running the whole application, navigating to option 1 and typing by hand.
What is missing is the ability to give a name to a piece of code and use that name whenever you need it. That is a function.
- What a function is
A function is a named block of code that is written once and executed as many times as necessary, simply by mentioning its name.
You have been using functions since the very first Python lesson: print(), input(), len(), int() and type() are built-in functions. Think about what they give you: you have no idea how print manages to make some letters appear on the screen — there is a considerable amount of work with the operating system in there — and yet you use it effortlessly. It is the abstraction from lesson 01-02, now applied to your own code. A well-made function gives you four things:
| Benefit | What it means in practice |
|---|---|
| Reuse | You write the algorithm once and invoke it from ten places |
| A name for an idea | ask_priority() reads at a glance; three lines of while have to be deciphered |
| Checking in pieces | You can test one function on its own, without starting the whole application |
| Localised change | You fix the algorithm in a single place and it is fixed everywhere |
Of the four, the second is the one beginners most underestimate. A function is not just saved typing: it is a new word in your program's vocabulary. When you write show_menu(), you stop thinking about print and start thinking about menus. Programming well consists, to a large extent, in building yourself a vocabulary that matches the problem.
- Syntax:
def, indented body and call
def, indented body and callDefining a function in Python has two parts: the header and the body.
def greet_the_team():
print("Good morning, Alba Studio team.")
print("Today we go through the pending tasks.")Let us break the header down, because every symbol plays its part:
defis the keyword that announces "I am about to define a function". It comes from define.greet_the_teamis the name. It is chosen just like a variable name:snake_case, no accents or spaces, descriptive.()are the parentheses, compulsory even when empty. Parameters will go inside them, the topic of the next lesson; today they will always be empty.:the colon closes the header and opens the block, exactly as in anifor awhile.
The body is everything indented underneath (four spaces, as always) and it ends where the indentation ends: the first line that returns to the left margin is already outside the function. Once defined, the function is called by writing its name followed by parentheses. If you add this below the previous block:
Good morning, Alba Studio team. Today we go through the pending tasks. Good morning, Alba Studio team. Today we go through the pending tasks.
Two call lines, two executions of the body. And notice that the definition prints nothing by itself: without those two calls, the program runs with no errors and no output whatsoever.
- Defining and calling are not the same thing
This distinction is the one that sticks in the throat most at the beginning. Defining is writing the recipe: Python reads the header, stores the body in memory under that name and carries on, without executing anything. Calling is cooking the recipe: Python goes and fetches the stored body and runs it now.
| Definition | Call | |
|---|---|---|
| Syntax | def pause(): + body |
pause() |
| How many times it is written | Once | As many as needed |
| Does it execute the body? | No | Yes |
| When it happens | When the file is read | At the exact point of the call |
The classic mistake is forgetting the parentheses when calling:
def pause():
input("Press Enter to continue...")
pause # <-- no parentheses: it calls NOTHING
pause() # <-- with parentheses: it does call itThe line pause without parentheses is valid Python and raises no error. It simply evaluates the name and obtains the function object, just as writing a bare x evaluates the variable x; the result is discarded and the program carries on. In the REPL you can see it clearly:
There it is: without parentheses, pause is a value of type function; with parentheses, it is an order to execute. This looks like a trap today, but it is one of Python's most powerful properties and in Functions as values we will take full advantage of it. The typical symptom in a real program: you run it, it does not fail, and quite simply nothing happens where you expected to see something. Check the parentheses first.
- Order of definition and execution: the
NameError
NameErrorPython runs the file from top to bottom. When it reaches a def, it registers the name; when it reaches a call, it looks that name up. Therefore: a function must be defined before the line that calls it.
A NameError means exactly that: you have used a name Python has not got registered. The three usual causes are that you have misspelled it (shwo_header), that you are using it before defining it or — we will see this in Variable scope — that it exists but is not visible from where you are.
An important nuance: what must come first is the call, not another definition. A function may mention in its body another one defined further down, because the body is not executed until it is called:
def prepare_screen():
show_header() # resolved when EXECUTING, not when defining
def show_header():
print("EasyTask - Alba Studio")
prepare_screen() # by now both exist: it worksThat is why the usual structure of a Python file is: constants at the top, then all the definitions, and the code that sets them in motion at the end. In Breaking a program down into functions we will formalise that structure.
- The flow of a call, step by step
When Python meets a call, five things happen in order: execution stops at that line; the return point is noted down, the exact place it will have to come back to; it jumps to the body and runs it whole, from top to bottom; when it finishes it goes back to the noted point; and it continues with the line after the call. That record of "where we were" is called the call stack: every call pushes a frame and every return pops it.
flowchart TD
A["Main: line 1"] --> B["Call to take_note()"]
B --> C["Body of take_note"]
C --> D["End of body: return"]
D --> E["Main: next line"]
Check it with a traceable example:
def take_note():
print("2. I enter the function")
print("3. I leave the function")
print("1. Before the call")
take_note()
print("4. After the call")The numbers come out in order because the main program waits: it does not run the last line until the function has finished completely. It is the "desk check" from 01-05, now with jumps. And if one function calls another, the stack grows: the main program waits for the first, which in turn waits for the second. That ability to nest calls holds up programs of any size, and taken to the extreme — a function that calls itself — it gives rise to recursion.
- Procedures and functions that compute:
None
NoneThere are two ways for a function to be useful. Functions that do — the classic procedures — are worth having for the effect they produce: printing on screen, asking for a value, waiting for a key. Functions that compute are worth having for the result they hand back to whoever called them, so that the caller decides what to do with it. Today we will write only the first kind; the return that makes the second possible arrives in the next lesson. But there is one detail you must see right now:
def show_header():
print("EasyTask - Alba Studio")
result = show_header()
print(result, type(result))Read it carefully. The function printed its text, but what it returned was None. In Python, every function that finishes without returning anything explicitly returns None, the "absence of value" value you met in 02-01. There are no functions that return nothing: there are functions that return None. From there comes a very common mistake:
print displays the text; it does not return it, so storing its result is of no use at all. Printing and returning are different operations, and confusing them is one of the hardest things to unlearn.
- Docstring and naming conventions
If the first line of a function's body is a loose string, Python treats it in a special way: it stores it as documentation. It is called a docstring and it is written with triple quotes.
def show_header():
"""Paint the application title on the screen."""
print("=" * 46)
print(f"{'EasyTask - Alba Studio':^46}")
print("=" * 46)With that you gain two things: the editor shows you that sentence as you type the call, and the function stays self-explanatory for whoever reads it in six months' time (which is nearly always you). From the REPL, help(show_header) prints the header followed by its docstring. For now, a minimal rule: a single line, starting with a verb, saying what the function does, not how it does it. The full format is the subject of Documentation and comments. And mind the difference from the # comment of 02-01: the comment explains a specific line to whoever reads the code; the docstring describes the whole function to whoever is going to use it without looking inside.
The name, for its part, is half the value of a function. The Python community's conventions are these:
| Rule | Good | Bad |
|---|---|---|
snake_case, lower case and underscores |
show_menu |
ShowMenu, showMenu |
| Starts with a verb in the infinitive | ask_priority |
priority, data |
| Describes what it does, not how | pause |
do_empty_input |
| No accents or odd characters | add_days |
añadir_días |
| One responsibility, no "and" in the name | validate_title |
validate_and_save_title |
The reason for the verb is that a function is an action: variables store things (nouns), functions do things (verbs). When you read pause() you understand instantly that something happens there; if you read pausing, you would hesitate between a variable and a call. Two prefixes you will see everywhere: show_* for whatever writes on the screen and ask_* for whatever queries the user. Adopting them makes your code predictable, which is higher praise than it sounds.
- EasyTask: the first three functions
For the moment we can only extract the pieces that need no input data and return nothing, because parameters are the next lesson. Even so there are three obvious candidates: the header, the menu and the pause.
# easytask.py - Alba Studio
# Version 0.6.1: first functions without parameters
WIDTH = 46
PRIORITIES = ("high", "medium", "low")
TEAM = ("Marta", "Luis", "Nuria")
OPTIONS = ("1", "2", "3", "4", "5")
def show_header():
"""Paint the framed title of the application."""
print("\n" + "=" * WIDTH)
print(f"{'EasyTask - Alba Studio':^{WIDTH}}")
print("=" * WIDTH)
def show_menu():
"""Show the header and the five available options."""
show_header()
print(" 1. Register / replace the task")
print(" 2. View the task card")
print(" 3. Change the priority")
print(" 4. Mark as completed")
print(" 5. Quit")
print("-" * WIDTH)
def pause():
"""Halt execution until the user presses Enter."""
input("\nPress Enter to return to the menu...")And the main loop, which now reads rather differently:
has_task = False
title = ""
assignee = ""
priority = ""
days = 0
completed = False
while True:
show_menu()
option = input("Choose an option (1-5): ").strip()
while option not in OPTIONS:
option = input("Invalid option. Choose 1-5: ").strip()
if option == "1":
pass # task registration (not extracted yet)
elif option == "5":
confirm = input("Are you sure you want to quit? (y/n): ").strip().lower()
if confirm == "y":
print("See you later. EasyTask is closing.")
break
pause()Four observations about what has just happened:
- Twelve lines of
printhave turned intoshow_menu(). The main loop now reads as what it is: show, read, dispatch, pause. The painting machinery still exists, but it has stopped getting in the way. show_menu()callsshow_header(). One function can lean on another, and that is how levels of abstraction are built.- The functions read the constants
WIDTH,OPTIONS… even though we do not pass them in. It works because they are global; in Variable scope you will see why, and why constants are the only acceptable exception to the rule of not depending on globals. pause()deliberately ignores whatinput()returns. Its worth lies in the effect: waiting. It is a textbook procedure.
Notice too the two blank lines between functions: it is Python's style convention (the PEP 8 guide, which we will see in 08-05) and it helps a great deal in telling blocks apart at a glance. What we still cannot extract are the repeated validation loops, because each one needs its own message and its own valid values, and it has to return the value it read.
Common Mistakes and Tips
Calling without parentheses. pause evaluates the function object and throws it away; pause() executes it. There is no error and no warning: quite simply nothing happens. If your program "skips" a step, look at the parentheses first. And if you call it before defining it, NameError: name 'x' is not defined: put all the definitions at the top and the code that starts the program at the end of the file.
Forgetting the colon or the indentation. def show_menu() without : gives SyntaxError; an unindented body gives IndentationError, the same one from 03-01. A function needs at least one indented line: if you do not yet know what to put, write pass.
Defining a function inside the loop. It is legal, but it is redefined on every round with no benefit whatsoever and it is distracting to read. Definitions go outside. And do not expect a function to run by itself: defining is not executing.
Believing that print returns the text. text = print("Hello") leaves text holding None. Displaying and returning are different things.
Tip: extract when code repeats twice, not three times. The second copy is already the signal; the third usually arrives with a subtle difference between copies, which is a bug waiting its turn. And test your functions in the REPL: paste the definition, call it and watch. You do not need to run the whole application to know whether a piece works, and that is precisely the gift functions give you.
Exercises
Exercise 1: Predict the output
Without running it, write down what this program prints and why. Pay attention to the parentheses.
Exercise 2: Three functions for the studio
Write alba_panel.py with three functions without parameters, each with its one-line docstring: show_welcome(), which prints a frame of 40 dashes, the text Alba Studio Panel centred and another frame; show_team(), which prints the three names of the team, one per line and preceded by a dash; and say_goodbye(), which prints a blank line and the message Panel closed.. Then call the three in order. The constant TEAM must be declared at the top and used in show_team().
Exercise 3: Fix the broken file
This file has four faults related to functions. Identify them and say what error each one produces, or whether it produces none.
show_title()
def show_title()
print("EasyTask")
def show_footer():
print("End of report")
show_footerSolutions
Solution 1. The output is A, B, Noted, Noted, None, each on its own line. Let us go through it: print("A") prints A; the line note without parentheses evaluates the function object and discards the result, without printing or failing; print("B") prints B; note() does run the body and prints Noted; the last call runs it once more (second Noted) and stores in result what it returns, which — as there is no return — is None.
Solution 2.
# alba_panel.py - Alba Studio
WIDTH = 40
TEAM = ("Marta", "Luis", "Nuria")
def show_welcome():
"""Paint the frame and the title of the panel."""
print("-" * WIDTH)
print(f"{'Alba Studio Panel':^{WIDTH}}")
print("-" * WIDTH)
def show_team():
"""List the names of the team, one per line."""
for person in TEAM:
print(f" - {person}")
def say_goodbye():
"""Close the panel with a farewell message."""
print()
print("Panel closed.")
show_welcome()
show_team()
say_goodbye()The for person in TEAM: is the traversal from 03-02: inside a function it works exactly as it does outside. And note the order: the three definitions at the top, the three calls at the end. If you moved show_welcome() to the first line of the file, you would get a NameError.
Solution 3.
| Fault | Consequence |
|---|---|
show_title() is called on line 1, before being defined |
NameError when running |
def show_title() without the colon |
SyntaxError |
The body of show_footer is not indented |
IndentationError |
show_footer at the end, without parentheses |
No error: it simply does not run |
The two syntax errors are detected before anything is run, so the program never even gets as far as producing the NameError. The corrected version adds the colon, indents the print of show_footer, and moves show_title() down to the end of the file next to show_footer(), this time with parentheses. The fourth fault is the most dangerous precisely because it does not fail: a program that stays quiet and does nothing is harder to diagnose than one that breaks with a clear message.
Conclusion
You now know how to make your own tools. A function is defined with def name(): and an indented body, it is called by writing name() — with parentheses, always — and it must be defined before the line that invokes it or Python will answer with a NameError. When you call it, the program stops, notes down where it was, runs the whole body and comes back exactly to the following point: that is the call stack. Functions that only produce an effect are called procedures and return None implicitly, because in Python every function returns something. And a one-line docstring, starting with a verb, leaves the function explained for whoever uses it.
With that, EasyTask has slimmed down a little: show_header(), show_menu() and pause() have taken the painting and waiting machinery out of the main loop. But the big problem is still untouched: the repeated validation loops. And we cannot extract them yet because a function like that has to be told which message to show and which values it accepts, and that function has to hand us back the validated value. Telling data to a function means parameters; having it hand us a result means return. Those two pieces, the ones that turn a function into something genuinely reusable, are the subject of the next lesson: Parameters and return values.
Fundamentals of Programming
Module 1: Introduction to Programming
- What is programming?
- History of programming
- Programming languages
- Development environments
- From problem to algorithm
Module 2: Core Concepts
- Variables and data types
- Operators and expressions
- Input and output
- Type conversion and data validation
Module 3: Control Structures
Module 4: Functions and Procedures
- Defining and using functions
- Parameters and return values
- Variable scope
- Breaking a program down into functions
- Functions as values: lambda and higher order
Module 5: Data Structures
- Lists and arrays
- Strings
- Dictionaries and sets
- Tuples and nested structures
- Saving data to files: text, CSV and JSON
Module 6: Basic Algorithms
Module 7: Objects and Code Organisation
- From data to objects: classes and instances
- Attributes, methods and the constructor
- Collections of objects
- Modules, packages and imports
Module 8: Good Practices and Tools
- Documentation and comments
- Debugging and error handling
- Version control
- Automated testing
- Style, readability and refactoring
