First-Class Functions
Notes based on Fluent Python, summarized and rephrased in my own words.
What is a “first-class” function?
In Python, functions are first-class objects. That means a function is just a regular value, like an int or a list, with a few important consequences:
- You can create functions at runtime.
- You can assign a function to a variable or store it in a data structure.
- You can pass a function as an argument to another function.
- You can return a function as the result of a function call.
def greet(name):
print(f"Hello, {name}!")
alias = greet # assign function object to another name
alias("World") # calls greet("World")This “functions as values” model is the foundation for many powerful patterns in Python, including callbacks, decorators, and higher-order functions.
Higher-order functions
A higher-order function is any function that either:
- takes one or more functions as arguments, or
- returns a function as its result.
Classic examples:
def apply_twice(func, value):
return func(func(value))
apply_twice(str.strip, " hello ") # "hello"
# Higher-order function returning a function
def make_power(exponent):
def power(base):
return base ** exponent
return power
square = make_power(2)
cube = make_power(3)
square(4) # 16
cube(2) # 8Higher-order functions let you capture a behavior (the function) separately from data, then plug that behavior into different places without rewriting logic.
Anonymous functions with lambda
The lambda keyword creates small, unnamed functions inline in an expression. They are limited to a single expression and are best used for very short, throwaway functions.
numbers = [1, 2, 3, 4]
# Using lambda as a key function
sorted_numbers = sorted(numbers, key=lambda x: -x) # sort descending
# Lambda as a quick transformation
squares = list(map(lambda x: x * x, numbers))For non-trivial logic, it’s usually clearer to define a normal def function with a meaningful name instead of using lambda.
What counts as “callable” in Python?
To check whether an object can be called like a function, use the built-in callable():
callable(len) # True
callable("hello") # FalseIn Python, several kinds of objects are callable:
- User-defined functions created with
deforlambda. - Built-in functions implemented in C (e.g.
len,time.time). - Built-in methods of built-in types (e.g.
dict.get). - Methods defined inside a class body.
- Classes themselves (calling a class constructs an instance via
__new__and__init__). - Instances of classes that define a
__call__method. - Generator functions (functions using
yield) which, when called, return generator objects.
Any object that implements __call__ behaves like a function:
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
triple = Multiplier(3)
triple(10) # 30This “callable object” pattern is useful when you want function-like behavior but also need to store state.
Functions are regular objects
Functions in Python have attributes just like instances of user-defined classes. For example, you can inspect a function using dir() and access attributes such as:
__name__: the function’s name.__doc__: the docstring.__defaults__: default values for positional/keyword parameters.__annotations__: parameter and return annotations.
You can also attach your own attributes to functions, though this is less common and mainly used by frameworks and libraries.
def process(item):
"""Process a single item."""
return item * 2
process.category = "data-cleanup" # custom attributeFlexible function parameters
Python’s function parameter system is very flexible. You can mix:
- Positional parameters.
- Keyword-only parameters.
- Variable-length positional parameters (
*args). - Variable-length keyword parameters (
**kwargs).
Here is a function that uses most of these features to create HTML tags:
def tag(name, *content, cls=None, **attrs):
"""Build one or more HTML tags as a string."""
if cls is not None:
attrs['class'] = cls
if attrs:
attr_str = ''.join(f' {attr}="{value}"' for attr, value in sorted(attrs.items()))
else:
attr_str = ''
if content:
# multiple content arguments -> multiple tags
return '
'.join(f'<{name}{attr_str}>{c}</{name}>' for c in content)
else:
# no content -> self-closing tag
return f'<{name}{attr_str} />'
# Examples
tag('br')
tag('p', 'hello')
print(tag('p', 'hello', 'world'))
print(tag('p', 'hello', id=33))
print(tag('p', 'hello', 'world', cls='sidebar'))
options = {"name": "img", "title": "Sunset Boulevard", "src": "sunset.jpg", "cls": "framed"}
print(tag(**options))Key ideas this example demonstrates:
*contentgathers any extra positional arguments into a tuple.clsis a keyword-only parameter (it can only be passed by name, not position).**attrsgathers any extra keyword arguments into a dict, often used for flexible options.- Using
*and**when calling a function unpacks a sequence or mapping into separate arguments.
Keyword-only parameters
To make an argument keyword-only (cannot be passed positionally), put it after a bare * or after *args in the function signature:
def f(a, *, b):
return a, b
f(1, b=2) # OK
# f(1, 2) # TypeError: b must be passed by keywordThis is useful when you want to:
- Add new options in a backward-compatible way.
- Make call sites more readable and self-documenting.
Inspecting function parameters
Sometimes frameworks need to inspect a function’s signature to discover:
- Parameter names.
- Defaults.
- Which parameters are positional-only, keyword-only, etc.
You can do this manually using low-level attributes such as __defaults__ and __code__, but the inspect module provides a cleaner API.
from inspect import signature
def clip(text, max_len=80):
"""Return text truncated at the nearest space before or after max_len."""
end = None
if len(text) > max_len:
space_before = text.rfind(' ', 0, max_len)
if space_before >= 0:
end = space_before
else:
space_after = text.find(' ', max_len)
if space_after >= 0:
end = space_after
if end is None:
end = len(text)
return text[:end].rstrip()
sig = signature(clip)
print(sig) # (text, max_len=80)
for name, param in sig.parameters.items():
print(param.kind, name, 'default =', param.default)Each Parameter object tells you:
- Its name.
- Its kind (positional-only, positional-or-keyword, var-positional, keyword-only, var-keyword).
- Its default value (or a special marker if none).
Frameworks (like web frameworks) can use this information to match HTTP request parameters to function parameters automatically.
Binding arguments to a signature
You can also bind actual arguments to a signature, letting inspect validate them:
import inspect
sig = inspect.signature(tag)
options = {"name": "img", "title": "Sunset Boulevard", "src": "sunset.jpg", "cls": "framed"}
bound = sig.bind(**options)
print(bound.arguments) # ordered mapping of parameter name → value
# If a required parameter is missing, bind() raises TypeError
# del options['name']
# sig.bind(**options) # TypeError: missing a required argument: 'name'This is a building block for writing libraries that do their own argument checking or auto-wiring.
Function annotations
Function annotations provide a structured place to attach metadata to parameters and return values. By convention, they are often used for type hints, but Python itself does not enforce them.
def clip_annotated(text: str, max_len: int = 80) -> str:
"""Truncate text at the nearest space around max_len."""
# implementation similar to clip()
return text[:max_len]
print(clip_annotated.__annotations__)
# {'text': <class 'str'>, 'max_len': <class 'int'>, 'return': <class 'str'>}Key points:
- Annotations are stored in the function’s
__annotations__dict. - The interpreter does not automatically check types; they are for tools (type checkers, IDEs, frameworks).
- You can store any Python object as an annotation, not just types.
Functional-style helpers: operator and functools
Although Python is not a pure functional language, the standard library includes tools that make a functional style convenient.
functools.reduce and operator
functools.reduce combines a sequence into a single value by repeatedly applying a function. Combined with operator functions, you can write very compact code:
from functools import reduce
from operator import mul
def factorial(n):
return reduce(mul, range(1, n + 1))
factorial(5) # 120Using operator.mul instead of lambda a, b: a * b often reads more clearly and is slightly faster.
Partially applying functions with functools.partial
functools.partial lets you freeze some arguments of a function, creating a new function with fewer parameters.
from operator import mul
from functools import partial
triple = partial(mul, 3) # new function: triple(x) == mul(3, x)
triple(7) # 21This is handy when:
- You want a simpler function interface tailored to a specific use.
- You need to adapt a general function to fit a callback API.
Why first-class functions matter
Having functions as first-class objects enables:
- Configurable behavior: pass different functions to change logic without rewriting code.
- Reusable algorithms: separate control flow from the operation performed on each item.
- Clean APIs: callbacks, hooks, and plugin points are just function parameters.
- Powerful libraries: decorators, dependency injection, and routing systems all rely on first-class functions.