First-Class Functions

In Python, functions are ordinary objects — they can be assigned to variables, stored in data structures, passed as arguments, and returned from other functions, just like any int or string.

Saying functions are 'first-class' in Python means a function is just another object: it has a type, it can be bound to a name, stored in a list or dict, passed into other functions, and returned as a result — with no special ceremony. This single property underlies decorators, callback-based APIs, functional-style tools like map/filter/sorted, and much of Python's flexibility. This topic focuses narrowly on that object-ness; decorators and closures as dedicated techniques are covered elsewhere.

Functions as Objects

A def statement creates a function object and binds it to a name in the current scope — exactly the same two-step process as x = 5 creates an int object and binds it to x. The function object has a type (function), attributes, and an identity, just like any other object.

def greet(name):
    return f"Hello, {name}"

print(type(greet))         # <class 'function'>
print(greet.__name__)      # 'greet'
print(callable(greet))     # True — it implements __call__

Assigning Functions to Variables

Because greet is just a name bound to a function object, you can bind additional names to that same object — calling any of them calls the same underlying function.

Note the missing () — greet refers to the function object itself; greet() calls it. This distinction trips up beginners constantly.

def greet(name):
    return f"Hello, {name}"

say_hi = greet          # another name for the SAME function object
print(say_hi("Amara"))  # Hello, Amara
print(say_hi is greet)  # True — identical object, two names

Storing Functions in Data Structures

Functions can live inside lists, tuples, and especially dicts — a very common pattern for dispatch tables that replace long if/elif chains.

def add(a, b): return a + b
def sub(a, b): return a - b
def mul(a, b): return a * b

operations = {
    "+": add,
    "-": sub,
    "*": mul,
}

def calculate(op, a, b):
    return operations[op](a, b)   # look up the function object, then call it

print(calculate("+", 3, 4))   # 7
print(calculate("*", 3, 4))   # 12

Passing Functions as Arguments

A function that accepts another function as an argument is called a *higher-order function*. This is how sorted(key=...), map(), and filter() work, and it's the foundation of every decorator.

def apply_twice(func, value):
    return func(func(value))

def increment(x):
    return x + 1

print(apply_twice(increment, 5))   # 7 — increment(increment(5))
print(apply_twice(lambda x: x * 2, 3))  # 12

Returning Functions from Functions

A function can construct and return another function, capturing arguments from the outer call in a closure. This is 'function factories' — one of the most powerful uses of first-class functions.

The closure mechanics here overlap with Scope & LEGB's Enclosing scope — see that topic for the full name-resolution picture.

def make_multiplier(factor):
    def multiplier(x):
        return x * factor   # captures 'factor' from the enclosing scope
    return multiplier

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5), triple(5))   # 10 15

Callable Objects

'Callable' is broader than 'function': any object implementing __call__ can be invoked with (), including class instances. This lets you build objects that behave like functions but carry their own persistent state.

class Multiplier:
    def __init__(self, factor):
        self.factor = factor
    def __call__(self, x):
        return x * self.factor

triple = Multiplier(3)
print(triple(7))          # 21 — called like a function
print(callable(triple))   # True
print(callable(42))       # False — an int is not callable

Higher-Order Functions in the Standard Library

Python's built-ins and standard library lean heavily on functions-as-arguments: map, filter, sorted(key=...), and functools.reduce all take a function and apply it across data.

from functools import reduce

numbers = [1, 2, 3, 4, 5]

squared = list(map(lambda x: x ** 2, numbers))
print(squared)   # [1, 4, 9, 16, 25]

evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)      # [2, 4]

total = reduce(lambda acc, x: acc + x, numbers, 0)
print(total)       # 15

Practical Usage

Dispatch tables (function-as-dict-value) replace unwieldy if/elif chains for command routing, event handlers, and strategy patterns. Passing functions as callbacks is standard in GUI frameworks, sorted/min/max key functions, and testing frameworks (fixtures, hooks).

handlers = {}

def on(event_name):
    def register(func):
        handlers[event_name] = func
        return func
    return register

@on("startup")
def handle_startup():
    print("Starting up...")

handlers["startup"]()   # Starting up...

Edge Cases

Functions, being objects, support attribute assignment (you can attach custom attributes to a function object) and identity comparison with is, but two separately-defined functions with identical bodies are never == to each other — they're distinct objects.

def f(): pass
f.custom_tag = "important"   # attaching an arbitrary attribute is legal
print(f.custom_tag)           # important

def g(): pass
def h(): pass
print(g == h)   # False — different function objects, even with identical bodies

Mental Model

Stop thinking of a function definition as a special language construct and start thinking of it as 'create an object, bind it to a name' — no different from x = SomeClass(). Once that clicks, passing functions around, storing them, and returning them all feel as unremarkable as doing the same with a list or a string.

Quick Reference

def f(): ...        # creates a function object, binds name 'f'
g = f                # another name for the same object
d = {"key": f}       # function stored as a dict value
h(f)                 # function passed as an argument (higher-order function)
def make(): 
    def inner(): ...
    return inner       # function returned from a function
callable(obj)         # True if obj implements __call__ (functions, methods, and instances with __call__)

Key Takeaways

Functions are ordinary objects in Python — they can be assigned, stored, passed, and returned exactly like any other value.

A higher-order function either accepts a function as an argument, returns a function, or both.

Dispatch dicts mapping keys to function objects are an idiomatic replacement for long if/elif chains.

callable(obj) is broader than 'is a function' — any object implementing __call__ (including class instances) is callable.

Two distinct function objects are never == even with identical code — equality for functions is identity-based.

operations = {"+": add(), "-": sub()} # calls the functions immediately!

operations = {"+": add, "-": sub} # stores the function objects themselves

Adding () calls the function right away and stores its return value in the dict, not the function itself; omitting () stores a reference to the callable for later invocation.

def make_adder(n): return add(n) # calls add immediately instead of returning a function def make_adder(n): def add(x): return x + n return add # returns the function object, uncalled

To return a function that can be called later, return the function object itself (no parentheses) rather than the result of calling it now.

if callback == None: callback = default_handler result = callback == expected_handler # comparing function identity with == if callback is None: callback = default_handler result = callback is expected_handler

Function comparisons should use 'is' for identity (are these the same object?) since function objects don't define a meaningful __eq__ beyond identity — using == works but 'is' communicates intent correctly and is the idiomatic check against None.

What does it mean to say Python functions are 'first-class objects'?

It means a function is a regular object like any other value: it can be assigned to a variable, stored in a data structure, passed as an argument to another function, and returned as a result, with no special restrictions.

What is a higher-order function? Give an example from the standard library.

A function that takes another function as an argument, returns a function, or both. Examples: sorted(key=...), map(), filter(), and functools.reduce() all accept a function argument.

Why does writing operations['+']() versus operations['+'] behave so differently in a dispatch dict?

operations['+'] retrieves the function object stored under that key without calling it; adding () calls that retrieved function immediately. Confusing the two either fails to invoke the function when needed, or invokes it prematurely (e.g. at dict-construction time instead of dispatch time).

What makes an object 'callable' in Python, beyond ordinary functions?

Any object whose class implements the __call__ method is callable — this includes functions, methods, classes themselves (calling a class invokes its constructor), and class instances that define __call__, letting them be invoked with () like a function.

Why are two separately defined functions with identical bodies never equal with ==?

Function objects don't override __eq__ to compare source code or behavior — equality falls back to default identity comparison, so only a function compared to itself (or another name bound to the exact same object) is equal.

How does 'return a function from a function' relate to closures?

When the returned inner function references variables from the enclosing function's scope, it forms a closure — the returned function retains access to those enclosing variables even after the outer function has finished executing.

What is the practical benefit of a dispatch dict over a long if/elif chain for routing commands to handler functions?

It's more concise, avoids repetitive comparison logic, is O(1) lookup instead of a linear chain of comparisons, and makes adding new commands as simple as adding a new dict entry rather than another elif branch.

Write a function compose(f, g) that returns a new function equivalent to f(g(x)), then use it to compose 'add one' and 'double'.

def compose(f, g): def composed(x): return f(g(x)) return composed add_one = lambda x: x + 1 double = lambda x: x * 2 add_then_double = compose(double, add_one) print(add_then_double(5))

12 — add_one(5) is 6, then double(6) is 12; compose returns a new function applying g first, then f.

What does this print?

False then True — f and g are distinct function objects with identical bodies but different identities, so f == g is False; f == f compares an object to itself, which is True.

Write a dispatch-table-based calculator function calculate(op, a, b) supporting '+', '-', '*', '/' using a dict of lambdas.

operations = { "+": lambda a, b: a + b, "-": lambda a, b: a - b, "*": lambda a, b: a * b, "/": lambda a, b: a / b, } def calculate(op, a, b): return operations[op](a, b) print(calculate("*", 6, 7))

42 — the dict maps '*' to a lambda that multiplies its two arguments, and calculate() looks it up then calls it with a and b.

True then 15 — Adder instances are callable because the class defines __call__, and calling add5(10) runs __call__(add5, 10), returning 10 + 5.

def f():
    return 1

def g():
    return 1

print(f == g)
print(f == f)
class Adder:
    def __init__(self, n):
        self.n = n
    def __call__(self, x):
        return x + self.n

add5 = Adder(5)
print(callable(add5))
print(add5(10))