Skip to main content

Functions as First-Class Objects

Source: Fluent Python, 2nd Edition — Chapter 7 by Luciano Ramalho

A first-class object is a program entity that can be created at runtime, assigned to a variable or stored in a data structure, passed as an argument to a function, and returned as the result of a function. Integers, strings, and dicts already work this way in Python — nothing special. The point of this chapter is that functions work this way too.

That single fact — functions are just objects — is what makes higher-order functions, decorators, and a good chunk of Python's standard library (sorted, functools, operator) possible.


Treating a function like an object​

A def statement creates a function object and binds it to a name, exactly the way any other assignment binds a name to an object. The function has attributes (__doc__ among them) and a type, just like any other value.

🧪 A function is an instance of the function class

fact = factorial doesn't copy the function — it's a second label on the same object, exactly like the aliasing from the last chapter. And because factorial is just an object, it can be handed straight to map(function, iterable), which calls the function once per item and returns an iterable of results.


Mini quiz — functions as objects​

Quiz

First-class functions

1 / 2

fact = factorial then fact(5)

Pick an answer to see if you are right.


Challenge: factorials via map​

Build the factorials from 0! to n! using map(factorial, range(n + 1)), not a loop.


Higher-order functions​

A function that takes a function as an argument, or returns one, is a higher-order function. sorted(iterable, key=...) is the one you'll reach for constantly: key is called once per item, and its return value — not the item itself — decides the order.

🧪 Sorting by length, then by reversed spelling

Note what key=reverse does not do: it never changes the words themselves, only what's compared while sorting — which is why words that rhyme (raspberry, strawberry, cherry) cluster together without any word being altered.


Mini quiz — higher-order functions​

Quiz

key= functions

1 / 2

sorted(words, key=len) sorts by

Pick an answer to see if you are right.


Challenge: sort by reversed spelling​

Write reverse(word) and use it as a key= function to sort words by their reversed spelling.


Modern replacements for map, filter, and reduce​

map and filter still exist, but a list comprehension (or generator expression) usually reads better and does the same job without a lambda. reduce moved from a built-in to functools in Python 3 — its most common use, summation, is better served by sum. all and any cover two other common reductions.

🧪 map/filter/reduce next to their comprehension equivalents

all(iterable) is True if every element is truthy (all([]) is True); any(iterable) is True if at least one element is truthy (any([]) is False).


Mini quiz — map/filter/reduce vs comprehensions​

Quiz

Reducing built-ins

1 / 2

The most common use of reduce — summation — is better written as

Pick an answer to see if you are right.


Challenge: sum without reduce​

Return the sum of the squares of the even numbers in nums, using sum() with a generator expression — no functools.reduce.


Anonymous functions​

lambda builds a function object inline. The catch: the body must be a single expression — no while, no try, no = assignment. Outside the narrow case of a short key= argument, a lambda usually hurts readability more than it helps.

🧪 The same sort, without naming reverse

If a lambda is hard to read, Fredrik Lundh's refactoring recipe is worth memorizing: write a comment explaining what it does, think of a name that captures that comment, convert the lambda to a def with that name, then delete the comment — the name alone should now carry the meaning.


Mini quiz — lambda​

Quiz

Anonymous functions

1 / 2

A lambda body can contain

Pick an answer to see if you are right.


Challenge: one-line reverse sort​

Sort words by reversed spelling using a lambda directly as key= — no separate def.


The nine flavors of callable objects​

() isn't reserved for def-made functions. As of Python 3.9, the data model lists nine callable types: user-defined functions, built-in functions (len), built-in methods (dict.get), methods, classes (calling one runs __new__ then __init__), class instances with __call__, generator functions, native coroutine functions (async def), and asynchronous generator functions (async def with yield). The safest way to check any of them is the callable() built-in.

🧪 callable() sees through all of them

str is a class, and classes are callable — calling one is how you get an instance. gen is a generator function: calling gen() doesn't run its body yet, it returns a generator object that only does work when you iterate it.


Mini quiz — callable types​

Quiz

Nine flavors

1 / 2

Calling a class, like str() or MyClass()

Pick an answer to see if you are right.


User-defined callable types​

Give a class a __call__ method and its instances become callable — a function-like object that can also carry state between calls. That combination is exactly what decorators (Chapter 9) need.

🧪 BingoCage: state plus call

bingo() is shorthand for bingo.pick() — that's all __call__ does. The state (self._items) survives between calls, which is the whole point: a plain function can't remember anything from one call to the next without extra machinery like a closure.


Mini quiz — call​

Quiz

Callable instances

1 / 2

Defining __call__ on a class means

Pick an answer to see if you are right.


Challenge: callable counter​

Build Counter with a __call__ that increments self.count and returns it, so each call yields the next count.


From positional to keyword-only parameters​

A parameter name after a bare *content (or a lone *) can only be passed by keyword — it will never soak up an unnamed positional argument. Since Python 3.8, a / in the signature marks everything before it positional-only — it can never be passed by keyword, matching how built-ins like divmod(a, b) have always worked.

🧪 Keyword-only b, and a small HTML tag builder

f(1, 2) would raise TypeError — b demands a keyword. class_ (trailing underscore because class is reserved) works the same way in tag: it can only ever be passed as class_=..., never as a stray positional argument swallowed by *content.


Mini quiz — parameter flavors​

Quiz

Keyword-only and positional-only

1 / 2

def f(a, *, b): f(1, 2)

Pick an answer to see if you are right.


Challenge: keyword-only greeting​

Make loud keyword-only by putting * before it in the signature.


The operator module​

Sorting or extracting by a field is common enough that writing lambda fields: fields[1] every time gets old. operator.itemgetter(i) builds that function for you (it works on anything with __getitem__, not just sequences); operator.attrgetter('a.b') does the same for attributes, including dotted, nested lookups; operator.methodcaller('name', *args) builds a function that calls .name(*args) on whatever it's given.

🧪 itemgetter and methodcaller

operator also has plain function equivalents for arithmetic — add, mul, and_, and friends — so reduce(mul, range(1, n + 1)) doesn't need a throwaway lambda a, b: a * b just to compute a factorial.


Mini quiz — operator module​

Quiz

itemgetter, attrgetter, methodcaller

1 / 2

itemgetter(1) builds a function that

Pick an answer to see if you are right.


Challenge: sort cities by country code​

Sort (name, cc, pop) tuples with operator.itemgetter(1) and print just the names, in order.


Freezing arguments with functools.partial​

functools.partial(fn, *args, **kwargs) returns a new callable with some of fn's arguments already filled in. It's how you adapt a function that needs N arguments to a spot that only ever hands it N - k.

🧪 Freezing the first argument of mul

mul takes two arguments, so it can't be passed straight to map(fn, iterable) where fn must take exactly one. partial(mul, 3) fixes the first argument at 3, producing a genuine one-argument function — triple — that map can call directly. The resulting functools.partial object exposes .func, .args, and .keywords if you ever need to inspect what got frozen.


Mini quiz — functools.partial​

Quiz

partial application

1 / 2

partial(mul, 3) returns

Pick an answer to see if you are right.


Challenge: freeze an argument​

Use functools.partial with operator.add to build a function that adds a fixed n to whatever it's given, then map it over a list.


Chapter summary​

Key takeaways
  1. Functions are first-class objects — assignable, storable, passable, returnable — exactly like ints or dicts.
  2. Higher-order functions (sorted(key=...), map, functools.partial) take or return functions. sorted/min/max with a key are the ones you'll actually use daily.
  3. List/generator comprehensions usually replace map/filter more readably; sum, all, any cover the common reduce cases.
  4. lambda is a single-expression function literal — great for a short key=, awkward for anything more.
  5. There are nine callable flavors in Python; callable() recognizes all of them.
  6. A class with __call__ makes its instances invocable like functions, while still holding state between calls.
  7. *args capture extra positionals, **kwargs capture extra keywords, a bare * (or a named *param) forces everything after it to be keyword-only, and / forces everything before it to be positional-only.
  8. operator.itemgetter / attrgetter / methodcaller replace one-off lambdas for extracting items, attributes, and calling methods.
  9. functools.partial freezes some of a callable's arguments, adapting it to fit an API that expects fewer.

Exercises​

Activity: Listcomp instead of map/filter

Return the same result as list(map(str.upper, filter(lambda w: len(w) > 3, words))), but as a single list comprehension.