When a Duck Calls Out • On Duck Typing and Callables in Python
Functions and classes have something in common: they're callable • Python's duck typing
What's "wrong" with the following statement?
You can use the
range()
function in Python to loop through a series of numbers.
Maybe nothing. Maybe it's just a technicality. Or maybe it's heresy.
We'll get back to this later. Let me first tell you about a conversation I joined briefly on social media this morning.
It all started with a picture of a pizza and a pair of scissors to cut it. One of the replies jokingly (I think) stated, "Eating a pizza with scissors is a crime."† This was a Python crowd, so I chimed with an attempt, probably unsuccessful, at witty humour: "It's like duck typing. It doesn't matter what the tool is, it's what it does that matters."
†I was doing my best to translate from Portuguese, a language I don't know, by doing a bit of patchwork from similarities with other languages. I was later corrected—the phrase said "cutlery", not "scissors"—cutlery has the word 'cut' in it, after all, so it's the same difference, right? Anyway, why let mere facts get in the way of a good story. So I'll stick with "scissors". Here's the conversation, if you're interested, and you should notice some well-known Python figures in this conversation, too: https://bsky.app/profile/mathspp.com/post/3lcp5zx7lj22n
Let's .cut_pizza()
Let's play with this idea a bit. For most of my life, whenever I ate a pizza, I used a Knife
. Let's define this class, sort of:
So, when it's dinner time, I'll need another useful class, Dinner
. And let's assume we also have a Pizza
class for, well, pizzas, of course. In this demo example, Pizza
doesn't need to have anything in it:
The Dinner
class defines two data attributes, .main_dish
and .knife
. Sure, it probably has more, but these are the only ones we need here.
The class also has an .eat()
method. Its purpose is clear enough! This method checks whether the .main_dish
is a Pizza
and uses the .knife
, an instance of Knife
, to .cut_pizza()
. Recall that .cut_pizza()
is a method that belongs to a Knife
object.
Right, so this code is a bit silly, I know. It's the ideas that matter. So, let's stick with it a bit longer.
Do you need an object of type Knife
to eat a Pizza
? It seems like it. The code above needs an instance of Knife
for you to cut the pizza.
No Knife
? No Pizza
!
But is that right? I recently bought a pizza cutter—one of those disc-shaped cutters you roll over the pizza:
A pizza cutter is not a knife, evidently. But a PizzaCutter
object also has a .cut_pizza()
method. It clearly works differently to Knife.cut_pizza()
—if you've ever used a knife and a pizza cutter, you'll agree that they need different skills!
But I need a Knife
to eat my Pizza
, no? Let's find out:
…but wait.
Dinner
needs a Knife
object as its second argument, right? Instead, you now have a PizzaCutter
object stored in the attribute my_dinner.knife
. Your intention, when you wrote the Dinner
class, was that the .knife
attribute would be an instance of Knife
. However, Python doesn't enforce the type of an object. Let's keep going and see what happens:
You call the .eat()
method on the my_dinner
object, which is an instance of Dinner
. Just a reminder that this code's only purpose is to play around with some ideas—this actual code doesn't do anything when you run it!
The .eat()
method has the following line:
Let's ignore the method's argument, self.main_dish
, which refers to a Pizza
object. This leaves three identifiers in this line:
self
refers to themy_dinner
object..knife
is aPizzaCutter
instance. Python hasn't complained that it's not aKnife
..cut_pizza()
needs to be a valid attribute for whatever is in.knife
. As it happens, aPizzaCutter
object also has a.cut_pizza()
method.
So, this line is valid…
…even though .knife
doesn't contain a Knife
object, as the Dinner
code originally intended.
Why did this code still work?
Even though the Dinner
class has an attribute called .knife
, it doesn't really need an object of type Knife
. All it needs is an object that has a .cut_pizza()
method.
What matters is not what the object is, but what it can do.
Duck Typing in Python
Let's repeat that:
What matters is not what the object is, but what it can do.
This is duck typing. You've heard it described as "if it quacks like a duck…" Well, you know that phrase already, right?
What a Dinner
object needs is any pizza-cutting device. A pizza-cutting device is any object that has the required interface. In this case, the only requirement is that the object has a .cut_pizza()
method. Any object that has this method is a pizza-cutting device and can be used in Dinner
.
Apparently, even this one:
Undoubtedly, this Scissors
class will have other functionality, maybe .cut_paper()
and .cut_cloth()
, but as long as it has a .cut_pizza()
method, it's a pizza cutting device!
Duck typing is central to Python. That's why we talk of iterables and sequences and mappings and so on. These are not data types. They're terms that represent a collection of behaviours. Any object that meets the requirements for any category can claim its place in the group.
The Knife
, PizzaCutter
, and Scissors
classes are actual data types like lists, tuples, and strings. And Knife
, PizzaCutter
, and Scissors
are all pizza-cutting devices, just like lists, tuples, and strings are all sequences.
A minute of your time. Thank you to all of you who read these articles. And thank you to the paid subscribers and those who contribute from time to time. As you can imagine, a lot of time and effort goes into these articles, all in the evenings and weekends.
If you want to contribute, you can upgrade to a paid subscription, either just for a few months ($5 per month) or using the yearly option ($50).
Alternatively, you can make a one-off contribution whenever you can, any amount you want.
Now, back to the article.
Functions and Classes • Callables
But let's get back to the question I asked at the start of this article:
What's "wrong" with the following statement?
You can use the
range()
function in Python to loop through a series of numbers.
Technically, range()
is not a function. You want proof?
Type range
in a REPL, and Python tells you it's a class named range
. This is not what happens with a function:
It clearly says that print
is a function here.
However, range
is a class. And therefore, range()
creates an instance of the class in the same way that PizzaCutter()
creates an instance of PizzaCutter
.
So, does it matter whether range
is a function or a class? It's even written using all lowercase letters, which is the convention used for functions, rather than UpperCamelCase (or PascalCase), which is how classes are normally written. The developers are giving us "permission" to think of it as a function.
What matters is what it does. You can call it using parentheses, pass some arguments to it, and it gives you a range
object back.
Python also has other classes masquerading as functions, such as zip
and enumerate
, among others.
Just like Knife
, PizzaCutter
, and Scissors
are all pizza-cutting devices and lists, tuples, and strings are all sequences, functions and classes are both callables.
In a duck typing language such as Python, often that's all that matters. Therefore, range
is callable, which is why you can use parentheses and include arguments, and it will return the required object.
So, if the interface of a pizza cutting device requires the .cut_pizza()
method and sequences need to have a specific set of dunder methods, what's required for an object to be callable?
It's a dunder method, of course: .__call__()
. Dunder methods, or special methods, as they're officially called, provide the interface for all of Python's core functionality.
And this means that functions and classes aren't the only things that are callable in Python. You can make any instance of a class callable by adding the .__call__()
special method in the class definition:
Dinner
is a class, so it's always callable. However, now even instances of Dinner
can be called since you defined the special method .__call__()
:
If you comment out the .__call__()
special method, this code will raise a TypeError
telling you that a Dinner
object is not callable.
This code doesn't do much. All the functionality is missing in the methods. The only one that's "complete" is the special method .__call__()
in Dinner
. It doesn't do much, either. But it confirms that it made instances of Dinner
callable. Here's the output from this code:
It's time to eat your dinner now!
Python also has a built-in function callable()
to check whether an object is callable:
And you can also add the following lines to your dinner and pizza cutting script:
This shows that both the class and its instance are callable:
True
True
Final Words
This article started by talking about duck typing, and then it zoomed in on one specific aspect of duck typing in Python: callables.
Understanding the duck typing philosophy and how it affects every aspect of Python programming is one of those key transitions when learning Python, a transition that gives you a much deeper understanding of what's going on behind the scenes, one of those moments when everything starts clicking into place.
In this article, I mentioned sequences and mappings and referred to other categories of data types, which are defined by the special methods they need. You can read more in this series of articles I wrote at the very beginning of The Python Coding Stack—you'll see that formatting and style are a bit different in these older articles: The Python Data Structure Categories Series. And there's more about duck typing in this Real Python article: Duck Typing in Python: Writing Flexible and Decoupled Code.
Now I fancy a pizza. Should I use a knife, a pizza cutter, or scissors?
Code in this article uses Python 3.13
The code images used in this article are created using Snappify. [Affiliate link]
For more Python resources, you can also visit Real Python—you may even stumble on one of my own articles or courses there!
And you can find out more about me at stephengruppetta.com
Further reading related to this article’s topic:
Duck Typing in Python: Writing Flexible and Decoupled Code – Real Python
Python's
.__call__()
Method: Creating Callable Instances – Real Python
Appendix: Code Blocks
Code Block #1
class Knife:
def __init__(self):
# Do some initialisation...
# Not relevant for this example
... # Ellipsis to ensure code can run
def cut_pizza(self, pizza):
# Code for knife to cut the pizza
# I know, I know, just play along, please!
...
Code Block #2
# ...
class Pizza:
...
class Dinner:
def __init__(self, main_dish, knife):
self.main_dish = main_dish
self.knife = knife # A `Knife` instance
def eat(self):
if isinstance(self.main_dish, Pizza):
self.knife.cut_pizza(self.main_dish)
# ...and some more stuff
Code Block #3
# ...
class Knife:
# ...
class PizzaCutter:
def __init__(self):
# Do some initialisation...
# Not relevant for this example
...
def cut_pizza(self, pizza):
# Code for pizza cutter to cut the pizza
...
# ...
Code Block #4
# ...
my_pizza_cutter = PizzaCutter()
my_pizza = Pizza()
my_dinner = Dinner(my_pizza, my_pizza_cutter)
Code Block #5
# ...
my_dinner.eat()
Code Block #6
self.knife.cut_pizza(self.main_dish)
Code Block #7
# ...
class Knife:
# ...
class PizzaCutter:
# ...
class Scissors:
def __init__(self):
# Do some initialisation...
# Not relevant for this example
...
def cut_pizza(self, pizza):
# Code for scissors to cut the pizza
...
# ...
Code Block #8
range
# <class 'range'>
Code Block #9
print
# <built-in function print>
Code Block #10
# ...
class Dinner:
def __init__(self, main_dish, knife):
self.main_dish = main_dish
self.knife = knife # A `Knife` instance
def eat(self):
if isinstance(self.main_dish, Pizza):
self.knife.cut_pizza(self.main_dish)
# …and some more stuff
def __call__(self):
print("It's time to eat your dinner now!")
# ...
Code Block #11
# ...
my_dinner = Dinner(Pizza(), PizzaCutter())
# Call the instance 'my_dinner'
my_dinner()
Code Block #12
callable(print)
# True
callable(range)
# True
import turtle
callable(turtle.Turtle)
# True
Code Block #13
# ...
print(callable(Dinner))
print(callable(my_dinner))
For more Python resources, you can also visit Real Python—you may even stumble on one of my own articles or courses there!
And you can find out more about me at stephengruppetta.com
True conaisseurs cut their pizza with ducks. Just saying.