Functions Are First-Class Objects Slides
Introduction
We have said twice now that a function is just an object — once in 2.1's motto, and again whenever we drew square sitting in the heap like any other value. This page finally cashes that in. Because a function is an ordinary object, you can do with it everything you can do with a number or a list: bind it to a name, store it in a list or dict, pass it to another function, and return it from one. That freedom is what "first-class" means, and it unlocks a powerful idea — the higher-order function — that the whole of 2.4 is built on.
As always, the code is runnable.
1. A function is an object Slides Slides
In Chapter 1 we checked an object's identity and type with id() and type(). A function answers both, just like an integer does — and nothing stops you from giving it a second name, or tucking it inside a list or a dictionary.
Example: functions are values you can move around
Key concept: first-class object
An object is first-class when the language puts no restrictions on it: it can be named, stored in data structures, passed as an argument, and returned as a result. In Python, functions are first-class — which is exactly what the rest of this page exploits.
2. Higher-order functions Slides Slides
Because a function is a value, a function can take another function as one of its arguments. A function that takes a function as an argument, or returns one, is called a higher-order function. You have already used some: sorted accepts a key function, and max does too.
The example below writes a tiny higher-order function of our own, and also passes the built-in len as an argument to sorted.
Example: passing a function as an argument
Key concept: higher-order function
A higher-order function is one that takes a function as an argument or returns a function as its result. Ordinary functions work on data; higher-order functions work on behavior.
Here is a more useful one, and it will stay with us for the rest of the page. Suppose we want to count how many times a function is called — a crude profiler. The counter has to survive between calls, and 2.2 gave us two ways to arrange that. There is also a third, which you can now recognise for what it is:
Example: call_count, counting with a shared default
That x=[0] is the shared mutable default from 2.1 §6 — the thing we called a bug — used here deliberately. Because the list is created once at def time and reused by every call, mutating x[0] is a way of remembering something between calls. It works, and you will meet it in real code, but it is a trick: the state is hidden in the signature, and any caller who passes their own x silently resets the tally.
The counter from 2.2 §6 does the same job honestly, with the tally bound to an enclosing frame instead of smuggled into a parameter. We return to that in §4, once closures have a name.
The more pressing limitation is different. Look at the last line of the body: func() — with empty parentheses. call_count can only count functions that take no arguments. call_count(print) prints a blank line, which is not much of a demonstration, and call_count(max) would fail outright. What we want is to hand print something to print.
The obvious repair is to accept one more parameter and pass it along:
Example: passing one argument through
Better — but only for functions taking exactly one argument. Two arguments and we are stuck again. We could add arg2, arg3, and give them defaults, but that is obviously the wrong road: we do not know in advance how many arguments the wrapped function needs, nor what they are called.
To solve this properly we need to look more carefully at how arguments reach parameters at all — and, first, at a piece of Python syntax that has nothing to do with functions.
3. How arguments reach parameters Slides Slides
3.1 Positional and keyword arguments
When a function has several parameters, you can supply the arguments two ways. Positional arguments match parameters by order. Keyword arguments name the parameter explicitly, so order stops mattering and the call reads clearly.
Example: positional vs keyword arguments
Pitfall: positional arguments must come first
Once a keyword argument appears in a call, every argument after it must also be
a keyword. power(base=2, 10) is a SyntaxError. Keep positionals first, then
keywords.
3.2 The star: packing and unpacking
Before we can solve the call_count problem, meet the * operator on its own ground. It has nothing to do with functions to begin with — it is about packing several values into one object and unpacking one object back into several names. You have already done both without noticing.
Start with the plainest case. Writing several values on the right of = packs them into a tuple; writing several names on the left unpacks them again:
Example: packing and unpacking, without any star
The rule so far is strict — as many names as values. Give it the wrong count and Python refuses, because it has no way to guess which name you meant to leave out:
The star relaxes exactly that restriction. A name marked with * says "however many are left over", and collects them into a list:
Example: one star absorbs the remainder
Two things follow from "however many are left." The starred name always ends up a list, even when it catches a single item or nothing at all. And there can be at most one star, since two would be ambiguous — *a, *b = 1, 2, 3 is a SyntaxError, because nothing decides where the first run stops and the second begins.
One more boundary is worth probing. Unpacking needs something to iterate over, so *a, b = [1] works while *a, b = 1 does not — a bare 1 is not a sequence, and Python says so: TypeError: cannot unpack non-iterable int object.
In-class exercise: predict the unpacking
Work these out on paper before running them. For each, give the value and type of every name, or say why it fails.
a = 1, 2, 3a, b, c = 1, 2, 3a, b = 1, 2, 3a, *b, c = 1, 2, 3, 4, 5*a, b, c = 1, 2, 3, 4, 5*a, *b = 1, 2, 3, 4, 5*a, b = 1
Now the direction that matters for calls. Used in front of an existing sequence, * does the reverse: it spreads that one object back out into separate values.
Example: a star spreads a sequence
Note how print(nums) and print(*nums) differ: the first hands print a single list object, the second hands it five separate arguments. So * on a name packs, and * on a value unpacks — one symbol, two opposite directions, and which one you get depends on which side of the = (or of the call) it sits.
The double star does the same for dictionaries. Where * spreads a sequence into positional values, ** spreads a mapping into key=value pairs — most visibly when merging two dicts:
Example: ** unpacks a mapping
3.3 Variable-length parameters: *args and **kwargs
Everything in §3.2 was about ordinary values. Now point the same two operators at a parameter list, and the call_count problem dissolves.
Passing an unknown number of arguments to a function takes two steps, and they are precisely the two directions you just saw:
- Packing — in the definition,
*argscollects however many positional arguments arrive into a tuple. - Unpacking — in the call inside the body,
*argsspreads that tuple back out into separate arguments for the function being wrapped.
Example: collecting arguments with * and **
The names args and kwargs are pure convention — the * and ** do the work. And note what each is: args is a tuple, kwargs is a dict, so everything you know about tuples and dicts from Chapter 1 applies inside the body.
Now call_count can count anything at all. It packs whatever arrives into args, and unpacks it again on the way through:
Example: call_count, finished
The sep=", " is there to prove the point. If args were passed along as a single tuple, print would show it with parentheses and commas of its own; because the star unpacked it, print received three separate strings and joined them with the separator we asked for. Try deleting the star and compare.
Key concept: *args and **kwargs
In a definition, *args gathers extra positional arguments into a tuple and
**kwargs gathers extra keyword arguments into a dict. In a call, * and
** spread a sequence or dict back into separate arguments. It is the same
packing/unpacking from §3.2, applied to a parameter list.
Two ordering rules apply, and both come from Python needing to tell arguments
apart: positional arguments must precede keyword arguments in a call, and
in a definition the order is ordinary parameters, then *args, then
**kwargs, with defaulted parameters after non-defaulted ones.
A wrapper that accepts *args, **kwargs and forwards both can stand in front of any function whatsoever — which is exactly the shape of a decorator in 2.4.
Example: a pass-through higher-order function
In-class exercise: variable-length arguments
- Write
my_sum(*args)that returns the sum of however many numbers it is given, and test it with two, then five numbers. **kwargsis a dict, so it has.keys(),.values()and.items(). Writesum_of_kwargsso thatsum_of_kwargs(Alice=5, Bob=3, Charlie=4)returns12, and a version that also reports who contributed the most.- Given
pair = (3, 4), callpowerfrom §3.1 aspower(*pair)and confirm it returns81. - Rewrite
call_countso it no longer needs thex=[0]default, using the counter from 2.2 §6 instead. Which version would you rather hand to someone else, and why?
4. Functions as return values: closures Slides Slides
The other half of "higher-order" is returning a function. When a function defines an inner function and returns it, the inner one carries its enclosing scope with it — the local names of the function that built it (recall the enclosing scope from 2.2). An inner function bundled together with the enclosing names it still refers to is called a closure.
Example: a function that builds functions
The picture below freezes make_linear(2, 1) at the very instant it returns. Its frame holds a → 2, b → 1, and the freshly built inner function line; the green arrow hands that function back to the global name double_plus_one.
Now look at what survives. The instant make_linear returns, the frame above is discarded — and yet by the time you call double_plus_one(5), a and b are still there. This is the heap idea from 2.1 carried to its conclusion: the frame was temporary, but the objects 2 and 1 are not freed, because the returned closure still refers to them. A closure is exactly that — a function bundled with a private pocket of the heap that it keeps alive.
The same idea repairs the function factory from 2.1 §7. make_f(i) in 2.2 §6.1 was a closure all along — each returned f kept its own captured i alive in exactly the way double_plus_one keeps a and b. Now the name for it is available.
To let the inner function change a captured name rather than only read it, use nonlocal — exactly the keyword from 2.2. That gives us the counter we built there, which we can finally call by its proper name:
Example: a closure that counts
Set that beside the x=[0] version of call_count from §2. Both remember a tally between calls; the difference is where the memory lives. The closure keeps it in a captured enclosing name, private and per-counter. The default-argument trick keeps it in the function's signature, visible to every caller and resettable by any of them. Same behavior, and one of them is honest about it.
Key concept: closure
A closure is an inner function together with the enclosing-scope names it still refers to. The closure keeps those objects alive after the outer call returns, giving the inner function a private, persistent memory.
Closures are more than a counter trick — they let you bundle data together with the behavior that acts on it, while keeping the data private. The example below builds simple game characters. Each make_player call captures its own hp and damage and returns a small bundle of functions that share them. Notice that hp is not one of the returned keys: the only way to change a character's health is through its own take_damage — nothing outside can reach in and set hp to a nonsense value.
Example: closures with private state (game characters)
def make_player(name, hp, damage):
def attack(other):
other["take_damage"](damage) # spend my damage on someone else
def take_damage(amount):
nonlocal hp
hp -= amount
def status():
print(f"{name}: {hp} hp")
return {"attack": attack, "take_damage": take_damage, "status": status}
bob = make_player("Bob", 100, 10)
charlie = make_player("Charlie", 100, 5)
bob["attack"](charlie)
charlie["status"]() # Charlie: 90 hp
bob["status"]() # Bob: 100 hp — untouched
All three inner functions close over the same hp and damage from one make_player call, so they cooperate: attack triggers the other player's take_damage, which updates the hp that status later reads. Two different players carry two independent sets of captured values. If that sounds like an object with private fields and methods — it is. Closures are one of the oldest ways to get encapsulation, and the seed of the classes you will meet in Chapter 3.
Deep dive: where captured variables actually live
A closure has to store its captured variables somewhere, and you can look
right at them. Python keeps them in cells attached to the function object,
reachable through __closure__:
print(charlie["take_damage"].__closure__) # a tuple of cell objects
print(charlie["take_damage"].__closure__[0].cell_contents) # 90 — the live hp
This is the heap idea made literal: the captured hp is an object held by a
cell on the inner function, which is exactly why it outlives the make_player
call that created it. And because attack, take_damage, and status came
from the same call, they share the same cell for hp — change it through
one and the others see the change at once. Sibling closures are tied together
by the heap cells they hold in common.
Deep dive: three ways to remember, and how to choose
Chapter 2 has now shown three ways to make a value survive between calls, and they are worth laying side by side because the choice is a real one.
A global (2.2 §6) is the bluntest: the tally is a module-level name that
anything can read or overwrite, and the function only makes sense next to it.
A mutable default — call_count's x=[0] — hides the tally in the
signature by exploiting the shared-default trap from 2.1 §6; it is compact,
it appears in real code, and it leaks, because any caller may pass their own
x and silently reset the count. A closure with nonlocal puts the tally
in a captured enclosing frame: private, one per counter, and it says what it
means.
Reach for the closure. Recognise the other two so you can read other people's code — and so you can see that all three are the same idea about where a name lives, which is the thread running through 2.1, 2.2 and this page.
5. lambda: a function with no name Slides
Often the function you want to pass somewhere is so small that giving it a def and a name feels heavy. A lambda builds a function inline, as an expression, with no name. Its body is a single expression whose value is returned automatically.
The syntax is lambda parameters: expression. These two definitions are equivalent:
Example: lambda is just a compact function value
Where lambda shines is passing a one-off function to a higher-order function, so you never have to name it.
Example: lambda as a key function
Pitfall: keep lambdas tiny
A lambda can hold only a single expression — no statements, no multiple
lines. That is by design: if a function needs more than one short expression,
give it a real def and a descriptive name. Reserve lambda for the little
throwaway functions you pass to sorted, max, map, and friends.
Summary
Functions in Python are first-class objects, so they can be named, stored, passed, and returned like any value:
| Idea | What it means |
|---|---|
| First-class | a function can be aliased, put in a list/dict, passed, and returned |
| Higher-order function | takes a function as an argument, or returns one |
*args / **kwargs |
collect extra arguments when defining; spread them when calling |
| Closure | an inner function plus the enclosing names it keeps alive |
lambda |
a small, anonymous function written inline as one expression |
These are not five separate tricks but one idea seen from different sides: a function is a value. Next, 2.4 Use Cases puts that idea to work in the five patterns your course is built around — decorators, recursion, map/filter/reduce, generators, and error handling — every one of them a higher-order function in disguise.