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

  1. The problem: repeated code with no name
  2. What a function is
  3. Syntax: def, indented body and call
  4. Defining and calling are not the same thing
  5. Order of definition and execution: the NameError
  6. The flow of a call, step by step
  7. Procedures and functions that compute: None
  8. Docstring and naming conventions
  9. EasyTask: the first three functions
  10. Common mistakes and tips
  11. Exercises
  12. Conclusion

  1. 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, lower and a while. 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.

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

  1. Syntax: def, indented body and call

Defining 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:

  • def is the keyword that announces "I am about to define a function". It comes from define.
  • greet_the_team is 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 an if or a while.

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:

greet_the_team()
greet_the_team()
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.

  1. 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 it

The 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:

>>> pause
<function pause at 0x7f3c9a1b2e50>
>>> type(pause)
<class 'function'>

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.

  1. Order of definition and execution: the NameError

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

show_header()               # ERROR: it does not exist yet

def show_header():
    print("EasyTask - Alba Studio")
NameError: name 'show_header' is not defined

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 works

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

  1. 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")
1. Before the call
2. I enter the function
3. I leave the function
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.

  1. Procedures and functions that compute: None

There 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))
EasyTask - Alba Studio
None <class 'NoneType'>

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:

text = print("Hello")     # text is None, not "Hello"

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.

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

  1. 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 print have turned into show_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() calls show_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 what input() 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.

def note():
    print("Noted")

print("A")
note
print("B")
note()
result = note()
print(result)

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_footer

Solutions

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.

© Copyright 2026. All rights reserved