Duck Typing in Python
Duck Typing is a dynamic typing concept in Python where an object’s suitability for an operation is determined by its methods and properties rather than its actual class. The phrase “If it looks like a duck and quacks like a duck, it must be a duck” perfectly explains this idea.
Understanding Duck Typing
In Python, we do not check an object’s type explicitly. Instead, we rely on its behavior. If an object implements the required methods, we can use it without checking its type.
Examples
1. Duck Typing in Action
Let’s start with a basic example. Here, we define two different classes: Duck
and Person
. Both classes have a quack
method.
Even though these classes are unrelated, they can be used interchangeably if a function expects a quack()
method. This is the essence of Duck Typing.
class Duck:
def quack(self):
print("Quack! Quack!")
class Person:
def quack(self):
print("I can quack like a duck!")
# Function that expects an object that can quack
def make_it_quack(entity):
entity.quack()
# Create objects
duck = Duck()
human = Person()
# Call function with different objects
make_it_quack(duck) # Output: Quack! Quack!
make_it_quack(human) # Output: I can quack like a duck!
Explanation:
Here, the function make_it_quack()
does not check the type of entity
. It simply calls the quack()
method. Since both Duck
and Person
have a quack()
method, the function works for both objects.
Output:
Quack! Quack!
I can quack like a duck!
2. Duck Typing with File-like Objects
Duck Typing is useful when working with file-like objects. If an object has a read()
method, it can be treated as a file, even if it’s not an actual file object.
class File:
def read(self):
return "Reading data from a file."
class StringIO:
def read(self):
return "Reading data from a string buffer."
# Function that reads from an object
def read_data(source):
print(source.read())
# Create objects
file = File()
string_buffer = StringIO()
# Call function with different objects
read_data(file) # Output: Reading data from a file.
read_data(string_buffer) # Output: Reading data from a string buffer.
Explanation:
The function read_data()
does not check if the argument is a file. It only checks if the object has a read()
method. This makes it flexible enough to work with files, string buffers, or any object that provides a read()
method.
Output:
Reading data from a file.
Reading data from a string buffer.
3. Duck Typing with Iterables
In Python, an object is considered iterable if it implements the __iter__()
or __getitem__()
method. Functions that iterate over objects rely on Duck Typing.
class CustomList:
def __init__(self, items):
self.items = items
def __iter__(self):
return iter(self.items)
# Function that iterates over an object
def print_items(iterable):
for item in iterable:
print(item)
# Create an object of CustomList
numbers = CustomList([1, 2, 3, 4])
# Pass it to the function
print_items(numbers) # Output: 1 2 3 4
Explanation:
Here, CustomList
does not inherit from the built-in list, but since it implements __iter__()
, it behaves like a list. The function print_items()
works with any iterable object.
Output:
1
2
3
4
4. Duck Typing with Arithmetic Operations
If an object implements arithmetic methods like __add__()
, __sub__()
, and __mul__()
, it can be used in mathematical expressions.
class Number:
def __init__(self, value):
self.value = value
def __add__(self, other):
return Number(self.value + other.value)
def __str__(self):
return str(self.value)
# Create number objects
num1 = Number(10)
num2 = Number(20)
# Perform addition
result = num1 + num2
# Print the result
print("Result of addition:", result)
Explanation:
Here, the Number
class defines how addition should work using __add__()
. As a result, two Number
objects can be added using the +
operator.
Output:
Result of addition: 30
5. When Not to Use Duck Typing
While Duck Typing is useful, there are cases where explicit type checking might be necessary, such as interacting with external APIs or handling user input.
def add_numbers(a, b):
if not (isinstance(a, (int, float)) and isinstance(b, (int, float))):
raise TypeError("Both arguments must be numbers")
return a + b
# Valid case
print(add_numbers(5, 3)) # Output: 8
# Invalid case
try:
print(add_numbers(5, "hello"))
except TypeError as e:
print("Error:", e)
Here, we enforce type checking using isinstance()
to avoid incorrect operations.
Output:
8
Error: Both arguments must be numbers