Duck Typing in Python: hasattr() and Abstract Base Class
In Python, there are three ways to implement duck typing: exception handling, the built-in hasattr()
function, and abstract base classes (ABCs).
This article explains the following contents.
- What is duck typing
- LBYL: Look Before You Leap
- Determine types using
type()
andisinstance()
- Determine types using
- EAFP: Easier to Ask for Forgiveness than Permission
- Exception handling with
try
andexcept
- Exception handling with
- The built-in
hasattr()
function- Use
hasattr()
for duck typing
- Use
- ABC: Abstract Base Class
- Use
collections.abc
for duck typing - Create classes that inherit from abstract base classes
- Other abstract base classes
- Use
What is duck typing
The Python official documentation's glossary provides the following explanation of duck typing.
A programming style which does not look at an object's type to determine if it has the right interface; instead, the method or attribute is simply called or used (“If it looks like a duck and quacks like a duck, it must be a duck.”) By emphasizing interfaces rather than specific types, well-designed code improves its flexibility by allowing polymorphic substitution. Duck-typing avoids tests using type() or isinstance(). (Note, however, that duck-typing can be complemented with abstract base classes.) Instead, it typically employs hasattr() tests or EAFP programming. Glossary - duck-typing — Python 3.11.2 documentation
In this article, we will first discuss an example that is not duck typing, such as:
- LBYL style (determining types using
type()
andisinstance()
)
Next, we will introduce examples that implement duck typing as mentioned in the quoted explanation:
- EAFP style (exception handling with try and except)
- Use
hasattr()
- Use Abstract Base Class (ABC)
We will use a simple example of printing the result of len()
for the target object.
Which method is better depends on individual circumstances and objectives, and will not be discussed here.
LBYL: Look Before You Leap
LBYL stands for "Look Before You Leap."
Look before you leap. This coding style explicitly tests for pre-conditions before making calls or lookups. This style contrasts with the EAFP approach and is characterized by the presence of many if statements. Glossary - LBYL — Python 3.11.2 documentation
Determine types using type()
and isinstance()
In LBYL style, type()
and isinstance()
are used to determine if an object is of a specific type. This style is familiar to those who have experience with languages like C. It is not duck typing.
For example, consider a function that outputs the result of len()
only when the object is a list. The implementation would be as follows:
def print_len_lbyl_list(x):
if isinstance(x, list):
print(len(x))
else:
print('x is not list')
print_len_lbyl_list([0, 1, 2])
# 3
print_len_lbyl_list(100)
# x is not list
print_len_lbyl_list((0, 1, 2))
# x is not list
print_len_lbyl_list('abc')
# x is not list
To support tuples as well, it would look like this:
def print_len_lbyl_list_tuple(x):
if isinstance(x, (list, tuple)):
print(len(x))
else:
print('x is not list or tuple')
print_len_lbyl_list_tuple([0, 1, 2])
# 3
print_len_lbyl_list_tuple(100)
# x is not list or tuple
print_len_lbyl_list_tuple((0, 1, 2))
# 3
print_len_lbyl_list_tuple('abc')
# x is not list or tuple
Other types, such as strings (str
), can be the target of len()
, but you need to explicitly add them to the conditions if you want to support them.
For more details on type()
and isinstance()
, see the following article:
EAFP: Easier to Ask for Forgiveness than Permission
EAFP stands for "Easier to Ask for Forgiveness than Permission."
Easier to ask for forgiveness than permission. This common Python coding style assumes the existence of valid keys or attributes and catches exceptions if the assumption proves false. This clean and fast style is characterized by the presence of many try and except statements. The technique contrasts with the LBYL style common to many other languages such as C. Glossary - EAFP — Python 3.11.2 documentation
This style involves executing code and handling exceptions if something goes wrong.
Exception handling with try
and except
In EAFP style, you can handle exceptions with try
and except
.
import numpy as np
def print_len_eafp(x):
try:
print(len(x))
except TypeError as e:
print(e)
print_len_eafp([0, 1, 2])
# 3
print_len_eafp(100)
# object of type 'int' has no len()
print_len_eafp((0, 1, 2))
# 3
print_len_eafp('abc')
# 3
a = np.arange(3)
print(a)
# [0 1 2]
print_len_eafp(a)
# 3
In the example above, print(len(x))
is executed for all objects that could be the target of len()
, including NumPy arrays and other third-party library classes.
For more details on exception handling, see the following article:
The built-in hasattr()
function
You can implement duck typing without exception handling by using the built-in hasattr()
function.
hasattr()
, as the name "has attribute" suggests, determines whether an object has an attribute or method and returns True
or False
. Specify an object as the first argument and a string of attribute or method names as the second.
l = [0, 1, 2]
print(type(l))
# <class 'list'>
print(hasattr(l, 'append'))
# True
print(hasattr(l, 'xxx'))
# False
Use hasattr()
for duck typing
The built-in len()
function is implemented to call the __len__()
method.
print(len(l))
# 3
print(l.__len__())
# 3
Therefore, you can determine if len()
can be executed by whether the object has a __len__()
method.
def print_len_hasattr(x):
if hasattr(x, '__len__'):
print(len(x))
else:
print('x has no __len__')
print_len_hasattr([0, 1, 2])
# 3
print_len_hasattr('abc')
# 3
print_len_hasattr(100)
# x has no __len__
a = np.arange(3)
print(a)
# [0 1 2]
print_len_hasattr(a)
# 3
In this way, duck typing can be implemented by using hasattr()
, as long as the target method or attribute is present, regardless of the actual type (if it quacks like a duck, it is a duck).
ABC: Abstract Base Class
Another approach to implement duck typing is to use abstract base classes (ABCs).
Abstract base classes complement duck-typing by providing a way to define interfaces when other techniques like hasattr() would be clumsy or subtly wrong (for example with magic methods). Glossary - abstract base class — Python 3.11.2 documentation
As introduced below, abstract base classes for collections and numbers are provided as built-in features.
Although not covered here, you can also define your own abstract base classes using the standard library's abc module.
Use collections.abc
for duck typing
The collections.abc
module provides abstract base classes for collections, i.e., objects like lists and dictionaries.
For example, Sized
is defined as an abstract base class with __len__()
, and Sequence
is defined as an abstract base class with __len__()
and __getitem__()
.
Determine the type of objects using these abstract base classes and the isinstance()
function.
With collections.abc.Sized
, objects that have the __len__()
method are targeted.
import collections
def print_len_abc_sized(x):
if isinstance(x, collections.abc.Sized):
print(len(x))
else:
print('x is not Sized')
print_len_abc_sized([0, 1, 2])
# 3
print_len_abc_sized('abc')
# 3
print_len_abc_sized({0, 1, 2})
# 3
print_len_abc_sized(100)
# x is not Sized
With collections.abc.Sequence
, lists and strings are covered, but sets and dictionaries are excluded because they do not have the __getitem__()
method.
def print_len_abc_sequence(x):
if isinstance(x, collections.abc.Sequence):
print(len(x))
else:
print('x is not Sequence')
print_len_abc_sequence([0, 1, 2])
# 3
print_len_abc_sequence('abc')
# 3
print_len_abc_sequence({0, 1, 2})
# x is not Sequence
print_len_abc_sequence({'k1': 1, 'k2': 2, 'k3': 3})
# x is not Sequence
With collections.abc.MutableSequence
, only mutable types are covered, so strings and tuples are not.
def print_len_abc_mutablesequence(x):
if isinstance(x, collections.abc.MutableSequence):
print(len(x))
else:
print('x is not MutableSequence')
print_len_abc_mutablesequence([0, 1, 2])
# 3
print_len_abc_mutablesequence('abc')
# x is not MutableSequence
print_len_abc_mutablesequence((0, 1, 2))
# x is not MutableSequence
In this way, more precise and flexible conditions can be specified. The same process is possible with multiple hasattr()
calls, but using abstract base classes can make the code simpler.
Create classes that inherit from abstract base classes
When creating classes that inherit from abstract base classes, there are benefits such as:
- An error occurs when required methods are not implemented
- Additional methods are automatically made available
Here is an example of inheriting collections.abc.Sequence
.
As listed in the "Abstract Methods" column in the table of the official documentation mentioned above, the implementation of __len__()
and __getitem__()
is required for collections.abc.Sequence
.
If not implemented, an error occurs during instance creation.
class MySequence(collections.abc.Sequence):
def __len__(self):
return 10
# ms = MySequence()
# TypeError: Can't instantiate abstract class MySequence with abstract methods __getitem__
When the required methods are implemented, the methods listed in the "Mixin Methods" column of the table in the official documentation (such as index()
and __reversed__()
) are automatically generated.
Note that in this example, to keep things simple, __len__()
is implemented to always return 10
, and __getitem__()
is implemented to simply return the index.
class MySequence(collections.abc.Sequence):
def __len__(self):
return 10
def __getitem__(self, i):
return i
ms = MySequence()
print(len(ms))
# 10
print(ms[3])
# 3
print(ms.index(5))
# 5
print(list(reversed(ms)))
# [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
print(isinstance(ms, collections.abc.Sequence))
# True
print(hasattr(ms, '__len__'))
# True
Even if the same methods are implemented, if the class does not inherit from collections.abc.Sequence
, additional methods are not available, and isinstance()
will return False
. Note that hasattr()
only checks the presence of methods, so in this case, it would return True
.
class MySequence_bare():
def __len__(self):
return 10
def __getitem__(self, i):
return i
msb = MySequence_bare()
print(len(msb))
# 10
print(msb[3])
# 3
# print(msb.index(5))
# AttributeError: 'MySequence_bare' object has no attribute 'index'
print(isinstance(msb, collections.abc.Sequence))
# False
print(hasattr(msb, '__len__'))
# True
Other abstract base classes
In addition to the collections.abc
module, there are other built-in modules that provide abstract base classes:
- numbers — Numeric abstract base classes — Python 3.11.2 documentation
- io — Core tools for working with streams — Python 3.11.2 documentation
- importlib — The implementation of import — Python 3.11.2 documentation
As mentioned above, it is also possible to create custom abstract base classes.