"I'm One of You!" Said the Function to the Other Objects
What does "functions are first-class objects in Python" mean?
I'm off on holiday in a few hours. No, I'm not travelling first-class. The "first class" in the subtitle refers to something else. But I haven't packed yet, so this article is brief!
Functions are first-class objects
You probably heard this phrase before. But what does it really mean?
Let's take a step back and start with a phrase you've definitely heard more times than you care to count:
Everything is an object
Let me pick a few characteristics of a Python object. As I have a flight to catch early in the morning, I'll give you the editor's choice rather than an exhaustive list of object properties.
You can assign one or more names to an object
Let's try with a list:
One object. Many names. You create an object, which is a list in this example, and assign the name great_food
to it. Then, you assign another name to the same object, what_im_eating_this_week
. In this case, there are two names, which I carefully chose so I can make a pun with the last expression I wrote! But there's only one object, which you can confirm when you get the same value returned by the id()
function. An alternative way to confirm they're the same object is to use the is
operator.
And you can do the same with a function. Note that # ...
shows that you carry on with the same REPL/Console session:
When you define the function, you create a new object of type 'function' and name it create_menu
. This is similar to when you created an object of type list and named it great_food
at the start of the first code block above. Similar, but not identical, of course, since functions are not lists. But they're both objects.
You even create a new name for the same function. The function names create_menu
and order_food
refer to the same function—the same object.
Every object is either truthy or falsy
Every object in Python can be used in places where Python is expecting a true-or-false value. A truthy object can be treated as True
and a falsy object as False
. You can confirm the truthiness of an object using the built-in bool()
:
Certain objects, like zero or empty sequences, are falsy. However, if an object hasn't been specifically designed to be falsy, then it's truthy:
Even though the definition of the class Holiday
is bare, a Holiday
instance is truthy. Therefore, if a function is an object, it must also have a truthiness value. I have to admit I had never tried this one out before today. Recall that create_menu()
is a function:
And it makes sense since functions are objects and objects are truthy unless they've been specifically designed to be falsy.
You're unlikely to ever need to rely on a function's truthiness, though!
Objects can be used as arguments in function calls
You surely used integers, floats, lists, dictionaries, and many more types of objects as arguments in function calls many times. Any object can be used as an argument in a function. But functions are themselves objects. Therefore, they're valid arguments, too. One place where you see this is when using the key
parameter in functions such as sorted()
:
The food items I'll be eating this week are ordered based on the length of their names. Maybe I should write a function to order them by calories, too!
The built-in function sorted()
has two arguments:
The first argument is the list
great_food
.The second argument is the built-in function
len
.
You only use the function name len
when you pass it as an argument and not the function call len()
. If you use the function call, the argument would be the value returned by len()
. However, you want to pass the function object itself and not the value returned when the function is called.
Objects can be returned by functions
Every function returns an object. Sometimes, that object is just None
. Sometimes, it's a tuple with several objects within.
And sometimes, a function returns…another function:
The first line in the function definition is a tuple called options
with two items. Both items in the tuple are functions. You use the function names, which represent the function objects.
The function surprise_print()
returns a random item from options
. Therefore, surprise_print()
returns either print
or create_menu
. Both are functions.
Let's call surprise_print()
a few times and assign its return value to the name display_items
. The value of display_items
is a function object–either print
or create_menu
:
Note how you call display_items
by adding parentheses, just like you'd call a function. The name display_items
refers to a function since suprise_print()
returns a function.
Can you guess what function display_items()
returned each of the three times I called it in the code above? In the first call, display_items()
returns the same output as print()
. Therefore, display_items
refers to the same object as print
. However, in the second and third calls, display_items
refers to the same object as create_menu
.
Objects can have attributes
When you learn about object-oriented programming, you learn about the types of attributes objects can have. There are methods and data attributes. They're both accessed using the dot notation.
And functions are objects. So, they also have attributes. Let's compare the attributes that are common between a list object and a function:
The object great_food
is a list and the object create_menu
is a function. You convert the list of attributes returned by .__dir__()
into a set and find the intersection between the two sets. There are plenty of attributes in common between a list object and a function object.
Let's also look at a method, which is a function:
You assign the .append
method object to the name the_method_object
. Note that there are no parentheses after the method name since you don't want to call the method but refer to the method object.
This is a bound method since it's bound to an instance of the class, which is a list in this case. The .__self__
attribute refers to the object this method is bound to.
And if you change the contents of the original list, the_method_object.__self__
will reflect this:
I need to go and pack for my holiday and go to sleep–I need to wake up at 4:00am. So here's one last thing. You can even add data attributes to a function object. Let's revisit the function surprise_print()
from earlier. First, let's try a rough-and-ready version:
You add a data attribute to surprise_print
, which is a function object. You create .counter
outside the function and increment it each time you call the function to keep track of how often you call it. You can see that each time you call surprise_print()
, the count increases.
Note that the function will raise an error if .counter
is not defined. You could instead achieve the same effect with a decorator:
I'll write about decorators in the future–and remember I've got a plane to catch soon… So, let's discuss it briefly. The function initialise()
:
Accepts a function as an argument
Creates the data attribute
.counter
for the function passed as an argument and assigns zero to itReturns the function that was passed in
Then, you use initialise()
as a decorator for surprise_print()
. This means that surprise_print
is passed to initialise()
when it's defined. Therefore, it will have the .counter
attribute added to it:
The first surprise_print()
call returned the create_menu
function, while the other two calls returned print
. Recall that these functions are chosen at random.
But note also that surprise_print()
checks whether the function has the attribute .counter
using the built-in function hasattr()
. Therefore, you can also use the function without the decorator. However, it won't have the .counter
attribute:
Final words
Hopefully, you're now convinced that functions are objects and they're not second class citizens in Python. They can do anything other objects can do.
Now, if only someone is willing to upgrade me to first-class on my flight tomorrow morning. Anyone?
PS: can you guess where I'm going on holiday from the examples in this article?
Reminder: last call for Beyond the Basics, the live-hybrid cohort course starting on the 15 April
Code in this article uses Python 3.12
Stop Stack
#57
The first hybrid cohort course starts in April: Beyond the Basics. This is included with membership of The Python Coding Place.
The Python Coding Book is out (Ebook available now, paperback in a few hours from Amazon) This is the First Edition, which follows from the "Zeroth" Edition that has been available online for a while—Just ask Google for ""python book"!
If you read my articles often, and perhaps my posts on social media, too, you've heard me talk about The Python Coding Place several times. But you haven't heard me talk a lot about is Codetoday Unlimited, a platform for teenagers to learn to code in Python. The beginner levels are free so everyone can start their Python journey. If you have teenage daughters or sons, or a bit younger, too, or nephews and nieces, or neighbours' children, or any teenager you know, really, send them to Codetoday Unlimited so they can start learning Python or take their Python to the next level if they've already covered some of the basics.
Each article is the result of years of experience and many hours of work. Hope you enjoy each one and find them useful. If you're in a position to do so, you can support this Substack further with a paid subscription. In addition to supporting this work, you'll get access to the full archive of articles. Alternatively, if you become a member of The Python Coding Place, you'll get access to all articles on The Stack as part of that membership. Of course, there's plenty more at The Place, too.
Appendix: Code Blocks
Code Block #1
great_food = [
"Cacio e Pepe", "Pizza al Taglio", "Saltimbocca", "Supplì",
]
type(great_food)
# <class 'list'>
what_im_eating_this_week = great_food
id(great_food)
# 4392467072
id(what_im_eating_this_week)
# 4392467072
great_food is what_im_eating_this_week
# True
Code Block #2
# ...
def create_menu(food_items):
print("On holiday I'll eat:\n", *food_items, sep="\n")
type(create_menu)
# <class 'function'>
order_food = create_menu
id(create_menu)
# 4392488240
id(order_food)
# 4392488240
order_food is create_menu
# True
Code Block #3
# ...
bool(0)
# False
bool(10)
# True
bool([])
# False
bool(great_food)
# True
Code Block #4
# ...
class Holiday:
pass
bool(Holiday())
# True
Code Block #5
# ...
bool(create_menu)
# True
Code Block #6
# ...
sorted(great_food, key=len)
# ['Supplì', 'Saltimbocca', 'Cacio e Pepe', 'Pizza al Taglio']
Code Block #7
# ...
import random
def surprise_print():
options = print, create_menu
return random.choice(options)
Code Block #8
# ...
# First call
display_items = surprise_print()
display_items(great_food)
# ['Cacio e Pepe', 'Pizza al Taglio', 'Saltimbocca', 'Supplì']
# # Second call
display_items = surprise_print()
display_items(great_food)
# On holiday I'll eat:
# Cacio e Pepe
# Pizza al Taglio
# Saltimbocca
# Supplì
# # Third call
display_items = surprise_print()
display_items(great_food)
# On holiday I'll eat:
# Cacio e Pepe
# Pizza al Taglio
# Saltimbocca
# Supplì
Code Block #9
# ...
set(great_food.__dir__()) & set(create_menu.__dir__())
# {'__setattr__', '__init__', '__doc__', '__getattribute__', '__class__',
# '__dir__', '__hash__', '__ge__', '__subclasshook__', '__repr__', '__gt__',
# '__new__', '__ne__', '__reduce__', '__eq__', '__lt__', '__str__',
# '__init_subclass__', '__reduce_ex__', '__format__', '__delattr__',
# '__sizeof__', '__le__'}
Code Block #10
# ...
the_method_object = great_food.append
type(the_method_object)
# <class 'builtin_function_or_method'>
the_method_object.__self__
# ['Cacio e Pepe', 'Pizza al Taglio', 'Saltimbocca', 'Supplì']
Code Block #11
# ...
great_food.pop(0)
# 'Cacio e Pepe'
the_method_object.__self__
# ['Pizza al Taglio', 'Saltimbocca', 'Supplì']
Code Block #12
def surprise_print():
surprise_print.counter += 1
options = print, create_menu
return random.choice(options)
surprise_print.counter = 0
surprise_print()
# <function create_menu at 0x105d00d30>
surprise_print.counter
# 1
surprise_print()
# <function create_menu at 0x105d00d30>
surprise_print.counter
# 2
surprise_print()
# <built-in function print>
surprise_print.counter
# 3
Code Block #13
# ...
def initialise(func):
func.counter = 0
return func
@initialise
def surprise_print():
if hasattr(surprise_print, 'counter'):
surprise_print.counter += 1
options = print, create_menu
return random.choice(options)
Code Block #14
#...
surprise_print()
# <function create_menu at 0x105d00d30>
surprise_print.counter
# 1
surprise_print()
# <built-in function print>
surprise_print.counter
# 2
surprise_print()
# <built-in function print>
surprise_print.counter
# 3
Code Block #15
# ...
def surprise_print():
if hasattr(surprise_print, 'counter'):
surprise_print.counter += 1
options = print, create_menu
return random.choice(options)
surprise_print()
# <function create_menu at 0x105d00d30>
surprise_print.counter
# Traceback (most recent call last):
# ...
# File "<input>", line 1, in <module>
# AttributeError: 'function' object has no attribute 'counter'