Skip to main content
In previous chapters, we learned how to define and call functions. In Python, functions are also objects. Since functions are objects, they can be assigned to variables, passed as arguments, returned from other functions, and stored in collections. These capabilities form the foundation for higher-order functions, decorators, and closures.

Learning Objectives

After completing this lesson, you will be able to:
  • Explain why functions are objects.
  • Pass and return functions.
  • Understand higher-order functions.
  • Build and use decorators.
  • Understand closures and their relationship with decorators.

Functions are Objects

Like integers, strings, lists, and dictionaries, functions are also objects. Therefore, a function can:
  • Be assigned to a variable.
  • Be passed as an argument.
  • Be returned from another function.
  • Be stored in a collection.
Languages that support these capabilities are said to support first-class functions.

Assigning a Function

Output
Notice the difference:

Storing Functions

Output

Practice

Exercise 1

Predict the output.
msg refers to the same function object as welcome.

Exercise 2

What is the difference between the following statements?
  • f = greet assigns the function object.
  • f = greet() calls the function and stores its return value.

Higher-Order Functions

A higher-order function is a function that:
  • Accepts one or more functions as arguments.
  • Returns a function.
Since functions are objects, they can be passed to and returned from other functions.

Passing Functions as Arguments

Output

Returning Functions

Output

Example

Output

Practice

Exercise 1

Predict the output.

Exercise 2

Predict the output.

Exercise 3

When is a function called a higher-order function?
A function is called a higher-order function if it:
  • Accepts one or more functions as arguments.
  • Returns a function.
Higher-order functions form the foundation for closures and decorators.

Closures

Sometimes we want a function to remember information from previous function calls. A normal function cannot do this because its local variables are destroyed when the function finishes executing.

Example: Normal Function

Output
Each time counter() is called, the local variable count is created again and initialized to 0. Therefore, the function cannot remember its previous state. To preserve the state between function calls, we can use a closure.

What is a Closure?

A closure is an inner function that remembers the variables of its enclosing function even after the enclosing function has finished executing. A closure is created when:
  • A function is defined inside another function.
  • The inner function uses variables from the outer function.
  • The inner function is returned.

Example

Output
Here, the variable count is preserved even after counter() has finished executing. Each call to increment() updates the same variable instead of creating a new one. The nonlocal keyword allows the inner function to modify a variable defined in the enclosing function.

Practice

Exercise 1

Predict the output.
The inner function remembers the value of message even after outer() has finished executing.

Exercise 2

Predict the output.
The variable count is preserved inside the closure and updated on each function call.

Exercise 3

Why do we use closures?
Closures allow a function to remember and preserve variables from its enclosing function even after the enclosing function has finished executing.
Closures are widely used for state preservation and form the foundation of decorators, where the wrapper function remembers the original function passed to the decorator.

Decorators

A decorator is a function that extends or modifies the behavior of another function without changing its original code. A decorator is a higher-order function because it:
  • Accepts a function as an argument.
  • Returns another function.

Creating a Decorator

Output
Instead of modifying greet(), the decorator returns a new function with additional behavior.

Using the @ Syntax

Python provides the @ syntax as a convenient way to apply decorators.
The above code is equivalent to:

Practice

Exercise 1

Predict the output.

Exercise 2

Which statement is equivalent to the following code?
The @ syntax is a shorthand for applying a decorator.

Decorating Functions with Parameters

The previous decorator works only for functions that do not accept any arguments.
Suppose we decorate a function that accepts parameters.
Our decorator is:
When we call:
Python actually executes:
Since wrapper() does not accept any arguments, Python raises an error.
One solution is to make the wrapper accept the same parameters.
This works only for functions having exactly two parameters. To make the decorator work with any function, Python provides argument packing.
Here,
  • *args collects all positional arguments.
  • **kwargs collects all keyword arguments.
  • func(*args, **kwargs) forwards all arguments to the original function.
Now the decorator can be applied to functions with any number of arguments.
Output
The wrapper() function is defined inside another function and remembers the original func even after the outer function has finished executing. This behavior was possible because of closure.

Practice & Exercises

To reinforce what you’ve learned in this section (Functions as objects, Higher-order functions, Closures, and Decorators), practice with these interactive notebooks:

Follow-Along Practice

Practice passing and returning functions, preserving state with closures using nonlocal, and creating custom decorators with arguments.💻 VS Code | 🚀 Colab | 📥 Download

Practice Exercises

Test your skills with exercises on higher-order function mapping, prefix greeting closures, timer decorators, argument uppercase decorators, and call counter decorators.💻 VS Code | 🚀 Colab | 📥 Download