In lesson 04-01 a detail turned up that we parked for later: writing pause without parentheses raises no error. Python answers with something like <function pause at 0x7f3c9a1b2e50>, and type(pause) returns <class 'function'>. Back then we treated it as a trap to avoid; now we are going to turn it around and make it a tool.
Because that detail means something important: in Python, a function is a value like any other. If it is a value, it can be stored in a variable, printed, passed as an argument to another function and returned from a function. And that opens up a different way of writing code: instead of deciding inside a function exactly what it will do, the behaviour is passed in from outside.
It is the module's last piece, and the one that lets us finish EasyTask off: the three functions ask_text, ask_option and ask_integer share the same skeleton — ask, check, insist — and differ only in which check they perform. If the check can be an argument, the three become one.
Contents
- A function without parentheses is a value
- Higher-order functions
- Anonymous functions with
lambda lambdaordef: when to use each- Returning a function: factories
- The callback pattern in EasyTask
- Higher order over collections: a preview
- Common mistakes and tips
- Exercises
- Conclusion of the module
- A function without parentheses is a value
Let us start by checking it in the REPL, with no tricks.
def greet():
print("Good morning, Alba Studio team.")
print(greet) # <function greet at 0x7f3c9a1b2e50>
print(type(greet)) # <class 'function'>
print(greet()) # runs the function and then prints NoneThe first two lines treat greet as data: they print it and check its type, just as you would with a number. The third treats it as an action: the parentheses are the call operator.
If it is a value, it can be assigned to another variable:
def show_header():
print("EasyTask - Alba Studio")
paint = show_header # no parentheses: we copy the reference
paint() # EasyTask - Alba Studio
print(paint is show_header) # TrueLook carefully at what has happened: the function has not been copied; instead there are now two names pointing at the same function object, exactly as you saw with references in 04-03. The is confirms it. From that moment on, paint() and show_header() are exactly the same call.
| Expression | What it is | What it holds |
|---|---|---|
show_header |
The function object | <function show_header at 0x…> |
show_header() |
A call | Whatever it returns (here, None) |
type(show_header) |
The type of the object | <class 'function'> |
This holds for built-in functions and for methods too: str.upper, len or input without parentheses are values you can store and pass around. It is the basis of everything that follows.
- Higher-order functions
A higher-order function is one that receives another function as a parameter or returns a function. It sounds abstract; an example clears it up in three lines.
def apply_format(text, transformation):
"""Apply the given transformation to the text and return the result."""
return transformation(text)
print(apply_format("book fair poster", str.upper))
print(apply_format("BOOK FAIR POSTER", str.lower))
print(apply_format("book fair poster", str.title))Inside apply_format, the parameter transformation is a local name pointing at whichever function was passed in; writing transformation(text) calls whatever has arrived. The function neither knows nor cares which one it is: the behaviour comes from outside.
And it works just the same with your own functions:
def mark_urgent(text):
"""Return the text surrounded by urgency markers."""
return f">>> {text.upper()} <<<"
def with_length(text):
"""Return the text followed by its number of characters."""
return f"{text} ({len(text)} characters)"
print(apply_format("book fair poster", mark_urgent))
print(apply_format("book fair poster", with_length))Note the decisive detail: in apply_format(text, mark_urgent), mark_urgent goes without parentheses. We are passing the function, not its result. If you wrote mark_urgent(...) you would be calling it right there and passing the string it returns, which is something completely different — and the number one source of errors in this lesson.
The advantage of this pattern is that it separates what repeats from what changes. apply_format is a skeleton; the specific transformation is a slot filled in on each call. You will see that this is exactly the problem with our three ask_* functions.
- Anonymous functions with
lambda
lambdaSometimes the function you want to pass is so small that giving it a name and three lines of def is out of proportion. That is what lambda is for: an expression that creates a function with no name.
The three parts: the word lambda, the parameters separated by commas (no parentheses) and, after the colon, a single expression whose value is returned automatically. You do not write return: it is implicit.
That lambda is equivalent, point for point, to def double(n): return n * 2. Its type is the same, function, and it is called in the same way. Where it really shines is inside another call, with no name given:
print(apply_format("book fair", lambda t: t.strip().capitalize()))
print(apply_format("book fair", lambda t: f"[{t}]"))
print(apply_format("book fair", lambda t: t.replace(" ", "_")))Three different behaviours without writing three defs. The limitation, which is a strong one, you have already seen: a lambda can only contain one expression. No if/elif over several lines, no loops, no assignments, no multiple statements. The ternary operator of 03-01 does fit, because it is an expression:
And a point of style worth taking on board right now: double = lambda n: n * 2 is correct but discouraged. If you are going to give the function a name, use def: it gives you a docstring, a name visible in error messages and room to grow. The lambda is meant for nameless functions, used on the spot.
lambda or def: when to use each
lambda or def: when to use each| Criterion | lambda |
def |
|---|---|---|
| Name | Anonymous | Has a name of its own |
| Body | A single expression | As many statements as needed |
return |
Implicit | Explicit |
| Docstring | Not allowed | Yes |
| In a traceback it appears as | <lambda> |
The function's real name |
| Typical use | Argument of another function, throwaway | Everything else |
| Reusable from several places | Badly | Well |
The practical decision rule fits into three questions. Are you going to use it more than once? Then def. Does it need more than one line, or does it deserve an explanation? Then def. Is it a trivial condition or transformation that reads better inside the call than as a separate definition three screens further up? Then lambda.
That last case is real and not rare: when the criterion is obvious, writing it in the same place where it is used saves the reader a trip to another part of the file. But as soon as the lambda starts having nested parentheses or a ternary inside another one, it has stopped clarifying and it is time to promote it to a def.
- Returning a function: factories
The other half of higher order is returning a function. A function that manufactures functions is informally called a factory, and it relies on the nested functions and the enclosing scope you saw in 04-03.
def create_greeting(studio_name):
"""Return a function that greets on behalf of the given studio."""
def greet(person):
return f"Hello {person}, welcome to {studio_name}."
return greet # no parentheses: we return the function
greet_alba = create_greeting("Alba Studio")
greet_sole = create_greeting("Sole Bakery")
print(greet_alba("Marta"))
print(greet_alba("Luis"))
print(greet_sole("Nuria"))Hello Marta, welcome to Alba Studio. Hello Luis, welcome to Alba Studio. Hello Nuria, welcome to Sole Bakery.
Analyse what happens, because it is subtler than it looks. create_greeting("Alba Studio") runs, defines the inner function greet and returns it; at that moment create_greeting finishes, and according to what you learnt in 04-03 its local variable studio_name ought to disappear. But it does not: the returned function carries with it the value it had. That is why greet_alba and greet_sole are two different functions, each with its own studio inside.
That mechanism has a name — a closure — and you do not need to master it today. It is enough to keep hold of the idea: a factory lets you create specialised versions of a function from a few configuration parameters, instead of repeating those parameters in every call.
def create_range_validator(minimum, maximum):
"""Return a function that checks whether a text is an integer in the range."""
def is_valid(text):
return text.isdigit() and minimum <= int(text) <= maximum
return is_valid
is_valid_day = create_range_validator(1, 365)
is_valid_hour = create_range_validator(0, 23)
print(is_valid_day("400")) # False
print(is_valid_day("4")) # True
print(is_valid_hour("22")) # TrueTwo complete validators from a single definition. Hold on to this idea: we are going to use it right now.
- The callback pattern in EasyTask
Look at the three functions from 04-02 with the flesh taken off:
value = input(message).strip()
while <whatever the check is>:
value = input(<whatever the warning is>).strip()
return valueThe three have the same skeleton and differ only in the check. You already know what to do: turn that check into a parameter. A function passed in so that another one can call it is called a callback, because whoever receives it calls it back when it is needed.
def ask_value(message, validator, warning="Value not valid."):
"""Ask for a value and return it once the validator approves it."""
value = input(message).strip()
while not validator(value):
print(warning)
value = input(message).strip()
return valueEight lines that replace the three previous functions. The contract is explicit: validator must be a function that receives a text and returns True or False. And now the validators, some with def and some with lambda, according to what the table in section 4 advises:
PRIORITIES = ("high", "medium", "low")
TEAM = ("marta", "luis", "nuria")
def is_valid_days(text):
"""Check that the text is an integer number of days between 1 and 365."""
return text.isdigit() and 1 <= int(text) <= 365
title = ask_value("Title : ",
lambda t: t != "",
"The title cannot be empty.")
assignee = ask_value("Assignee : ",
lambda t: t.lower() in TEAM,
f"It must be one of: {', '.join(TEAM)}.")
priority = ask_value("Priority : ",
lambda t: t.lower() in PRIORITIES,
"Priority: high, medium or low.")
days = int(ask_value("Days (1-365) : ", is_valid_days,
"It must be an integer between 1 and 365."))Four observations about this code:
- The first three validations are trivial and go in a
lambda, written right where they are used: you can see at a glance what counts as valid for each field. The one for days has two chained checks and deserves adefwith a name and a docstring. is_valid_daysgoes without parentheses. We are passing the function, not calling it. It is the mistake made most often here.- The warning is a parameter with a default value (04-02), so each field can explain its own criterion without being forced to do so every time.
ask_valuealways returns text, and the conversion to an integer is done outside, at the call. It is consistent with 04-04: the input function validates, and whoever uses it decides which type they want.
Has the program improved? It depends what you value. The honest table:
Three ask_* functions (04-02) |
One ask_value with a callback |
|
|---|---|---|
| Functions to maintain | 3 | 1 |
| Flexibility | Only the three foreseen cases | Any criterion imaginable |
| Readability of the call | Very high (ask_integer(m, 1, 365)) |
Medium: you have to read the lambda |
| Difficulty for a beginner | Low | Medium |
There is no absolute winner, and it is worth saying so plainly: higher order is a tool, not an automatic improvement. For a small program with three data types, the three specific functions read better. When validation criteria multiply and change often, ask_value wins hands down. What matters is that you can now choose, and that you recognise the pattern when you see it in other people's code, where it is common.
- Higher order over collections: a preview
The place where higher order is used daily is over collections of data: sorting a list of tasks by priority, keeping only Nuria's, transforming them all in one go. The classic tools are sorted with its key parameter, and map and filter.
As you do not know lists yet, let us look at it with what you do have: a string, which is a sequence of characters. (sorted returns a list of characters, so we join them back together with "".join(...) in order to print them as text; lists arrive in module 5.)
print("".join(sorted("nuria"))) # ainru
print("".join(sorted("Nuria"))) # Nairu
print("".join(sorted("Nuria", key=str.lower))) # aiNru
print(max("Marta", "Luis", key=len)) # MartaNotice key=str.lower: it is a keyword argument whose value is a function. sorted calls it once per character and sorts by whatever it returns, not by the original character. That is why the third line ignores upper and lower case whereas the second puts the N at the front — capitals come first in character order. And max(..., key=len) picks the longest string instead of the alphabetically greatest one: the same old max, with a different criterion injected from outside.
That is the whole pattern. sorted does not know how to sort tasks by priority, but it does know how to sort anything at all if you tell it with a function what to look at in each element. When you reach Lists and arrays you will have real collections to apply it to, and in Sorting algorithms you will see how sorting works on the inside. map and filter, which apply a function to every element or select those that satisfy it, are also studied alongside collections; for now it is enough that you recognise the shape when you come across it.
Common Mistakes and Tips
Passing the call instead of the function. apply_format(text, mark_urgent()) runs mark_urgent right there — and with no arguments, so it gives a TypeError — and passes its result. To pass the function, write it without parentheses.
Forgetting the parentheses inside the higher-order function. The mirror-image error: if inside you write return transformation instead of transformation(text), you return the function object. Symptom: <function … at 0x…> is printed where you expected a text.
Cramming too much into a lambda. If you need a multi-branch if, a loop or an assignment, it does not fit: SyntaxError. Use def.
Giving a lambda a name. double = lambda n: n * 2 works, but you lose the docstring and a useful name in errors. If it has a name, make it a def.
Confusing the parameter with the function that arrives. Inside ask_value, validator is a local name: it may receive a lambda today and a def tomorrow. The function only requires it to be callable with a text and to return a boolean.
Tip: document the callback's contract. In the docstring, say what the function they pass you must receive and what it must return. Without that, whoever uses your function will have to read the body.
Tip: do not use higher order because it looks sophisticated. If two ifs solve the problem more clearly, then two ifs. Abstraction is paid for in reading effort, and it only pays off when it saves real repetition.
Exercises
Exercise 1: Predict the output
Say what each line prints and, where applicable, what error is produced and why.
def frame(text):
return f"<<{text}>>"
f = frame
print(f("Sole Bakery"))
print(f)
print(frame is f)
def apply(value, function):
return function(value)
print(apply("Sole Bakery", frame))
print(apply("Sole Bakery", frame("Vidal")))Exercise 2: A factory of labellers
Write create_labeller(prefix, width), a function that returns another function. The returned function receives a text and returns the prefix left-aligned in 10 characters followed by the text right-aligned in width characters, using the f-string formatting of 02-03. Use it to create two labellers — one for urgent tasks with the prefix [URGENT] and width 30, and another for notes with the prefix [note] and width 24 — and try them out with "Book fair poster".
Exercise 3: Validators for EasyTask
Using ask_value(message, validator, warning) from section 6, write the call expressions needed to ask for these three values, deciding in each case whether the validator should be a lambda or a def and justifying it:
- A client code: exactly four digits.
- A yes-or-no answer:
yorn, regardless of case. - A progress percentage: an integer between 0 and 100, and also a multiple of 5.
Solutions
Solution 1.
<<Sole Bakery>> <function frame at 0x...> True <<Sole Bakery>> TypeError: 'str' object is not callable
f = frame creates a second name for the same object, so f("Sole Bakery") frames it just the same and frame is f is True. print(f) shows the representation of the function object. The first call to apply passes the function and works. The last one passes frame("Vidal"), which is evaluated before the call and holds "<<Vidal>>": inside apply, function is a string, and on trying function(value) Python protests with 'str' object is not callable. It is the same message you saw when shadowing a built-in in 04-03, and for the same reason: something that is not a function is being called.
Solution 2.
def create_labeller(prefix, width):
"""Return a function that labels texts with the given prefix and width."""
def label(text):
return f"{prefix:<10}{text:>{width}}"
return label
label_urgent = create_labeller("[URGENT]", 30)
label_note = create_labeller("[note]", 24)
print(label_urgent("Book fair poster"))
print(label_note("Book fair poster"))The inner function label reads prefix and width from the enclosing scope (the E of LEGB, 04-03) and carries on remembering them after create_labeller has finished. Compare this approach with passing prefix and width on every call: the factory saves you repeating them, and into the bargain makes it clear that an urgency labeller is one thing and a note labeller is another.
Solution 3.
code = ask_value("Client code: ",
lambda t: t.isdigit() and len(t) == 4,
"It must be exactly four digits.")
answer = ask_value("Confirm? (y/n): ",
lambda t: t.lower() in ("y", "n"),
"Answer y or n.")
def is_valid_progress(text):
"""Check that it is an integer from 0 to 100 and a multiple of 5."""
if not text.isdigit():
return False
value = int(text)
return 0 <= value <= 100 and value % 5 == 0
progress = int(ask_value("Progress (0-100): ", is_valid_progress,
"Integer from 0 to 100, a multiple of 5."))The first two criteria fit comfortably into one expression and read better inside the call, so lambda it is. The third needs to convert the text into a number after checking that it can be converted, and that is two steps with an intermediate variable: impossible in a readable lambda, and besides it deserves a docstring explaining the multiple-of-5 rule. That if not text.isdigit(): return False is, by the way, the guard clause of 04-02 doing its job: without it, int(text) would blow up with a ValueError at the sight of any letter.
Conclusion of the module
Functions are data too. Without parentheses, a function name is a value of type function that can be printed, assigned to another variable and passed around; with parentheses, it is a call. From there come higher-order functions: those that receive a function as a parameter — you supply the skeleton, the caller supplies the specific behaviour — and those that return a function, the factories, capable of creating specialised versions that remember their configuration. For tiny throwaway functions there is lambda, limited to a single expression and with no explicit return; for everything else, def. And the callback pattern — passing the check as an argument — has unified EasyTask's three ask_* functions into a single ask_value.
That closes module 4. You know how to define and call functions, and to tell the definition from the call (04-01). You know how to give them positional, keyword and default-valued parameters, and how to make them return one or several values with return, understanding why returning nearly always beats printing (04-02). You know where each name lives thanks to the LEGB rule, why changing globals are a problem and why constants are not (04-03). You know how to break down a whole program with checkable criteria: one responsibility per function, uniform levels of abstraction, input and output kept apart from logic, DRY without over-fragmenting, and a main() protected by if __name__ == "__main__": (04-04). And you know that functions are values that get passed and returned (04-05).
easytask.py has reached version 0.7: constants at the top, functions grouped by families, a twenty-two-line main() that reads like a table of contents, zero duplication and eight pieces reusable in any other program. It is an enormous leap from the wall of v0.6.
And yet, it still manages one single task. Marta cannot have the book fair poster, the Solé Bakery menu and the Vidal account job all at once: registering one destroys the previous one. The six loose variables — title, assignee, priority, days, completed, has_task — travel together from function to function because they are, in truth, one single thing without a name. And show_card(title, assignee, priority, days, completed) with its five parameters is the most visible symptom.
Both things — storing many tasks and grouping the data of each one — are solved by the same means: data structures. That is module 5, which begins with Lists and arrays: the tool that will finally let EasyTask manage the real work of Alba Studio, walk through it with a for, search in it, sort it by priority and, when we get to files, still have it there tomorrow morning.
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
