Duck Typing in Python: Writing Flexible and Decoupled Code

Duck Typing in Python: Writing Flexible and Decoupled Code

by Leodanis Pozo Ramos Feb 26, 2024 intermediate python

Python makes extensive use of a type system known as duck typing. The system is based on objects’ behaviors and interfaces. Many built-in classes and tools support this type system, which makes them pretty flexible and decoupled.

Duck typing is a core concept in Python. Learning about the topic will help you understand how the language works and, more importantly, how to use this approach in your own code.

In this tutorial, you’ll learn:

  • What duck typing is and what its pros and cons are
  • How Python’s classes and tools take advantage of duck typing
  • How special methods and protocols support duck typing
  • What alternatives to duck typing you’ll have in Python

To get the most out of this tutorial, you should be familiar with several Python concepts, including object-oriented programming, classes, special methods, inheritance, and interfaces.

Getting to Know Duck Typing in Python

In object-oriented programming, classes mainly aim to encapsulate data and behaviors. Following this idea, you can replace any object with another if the replacement provides the same behaviors. This is true even if the implementation of the underlying behavior is radically different.

The code that uses the behaviors will work no matter what object provides it. This principle is the basis of a type system known as duck typing.

Duck Typing: Behaving Like a Duck

You’ll find many different definitions of duck typing out there. At its core, this coding style is based on a well-known saying:

If it walks like a duck and it quacks like a duck, then it must be a duck.

Extrapolating this to programming, you can have objects that quack like a duck and walk like a duck rather than asking whether those objects are ducks. In this context, quack and walk represent specific behaviors, which are part of the objects’ public interface (API).

Duck typing is a type system where an object is considered compatible with a given type if it has all the methods and attributes that the type requires. This type system supports the ability to use objects of independent and decoupled classes in a specific context as long as they adhere to some common interface.

Duck typing is pretty popular in Python. The language documentation defines duck typing as shown below:

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. (Source)

Here’s a quick example that involves birds that can swim and fly:

Python birds_v1.py
class Duck:
    def swim(self):
        print("The duck is swimming")

    def fly(self):
        print("The duck is flying")

class Swan:
    def swim(self):
        print("The swan is swimming")

    def fly(self):
        print("The swan is flying")

class Albatross:
    def swim(self):
        print("The albatross is swimming")

    def fly(self):
        print("The albatross is flying")

In this example, your three birds can swim and fly. However, they’re completely independent classes. Because they share the same interface, you can use them in a flexible manner:

Python
>>> from birds_v1 import Duck, Swan, Albatross

>>> birds = [Duck(), Swan(), Albatross()]

>>> for bird in birds:
...     bird.fly()
...     bird.swim()
...
The duck is flying
The duck is swimming
The swan is flying
The swan is swimming
The albatross is flying
The albatross is swimming

Python doesn’t care about what object bird is holding at a given time. It just calls the expected methods. If the object provides the method, then the code works without breaking. That’s the flexibility that duck typing offers.

The duck typing system is pretty popular in Python. In most cases, you shouldn’t worry about making sure that an object is of the right type for using it in a certain piece of code. Instead, you can rely on objects that quack like ducks and walk like ducks.

Duck Typing and Polymorphism

In object-oriented programming, polymorphism allows you to treat objects of different types as the same general type. Polymorphism aims to enable code to work with objects of various types through a uniform interface (API), which helps you write more general and reusable code.

You’ll find different forms of polymorphism in object-oriented programming. Duck typing is one of them.

Duck typing enables polymorphism, where you can use objects of different types interchangeably, provided that they implement certain behaviors, also known as their interface. An essential feature of this type of polymorphism is that the objects don’t have to inherit from a common superclass, which makes code less rigid and more adaptable to change.

In Python, duck typing is a pretty popular way to support polymorphism. You just need to decide which methods and attributes a particular class has. Because Python is a dynamically typed language, there are no type-checking restrictions.

Understanding the Pros and Cons of Duck Typing

Duck typing offers a lot of flexibility to you as a programmer, mainly because you don’t have to think of complex concepts like inheritance, class hierarchies, and the relationships between classes. There’s a reason it’s so popular in Python! Here are some of its pros:

  • Flexibility: You can use different objects interchangeably based on their behavior without worrying about their types. This promotes modularity and extensibility in your code.
  • Simplicity: You can simplify code by focusing on the required behavior rather than thinking of specific types, classes, and the relationships between them. This allows for more concise and expressive code.
  • Code reuse: You can reuse one or more of your classes in other apps without having to export a complex class hierarchy for the classes to work. This facilitates code reuse.
  • Easier prototyping: You can quickly create objects that exhibit the necessary behavior without complex type definitions. This is useful during the initial stages of development, where you may not have fully fleshed out class hierarchies or interfaces.

However, not everything is perfect. Duck typing isn’t always the right choice for your code. Here are some cons of duck typing:

  • Potential runtime errors: You might face errors related to missing methods or attributes, which may only appear at runtime. This can lead to unexpected behavior or crashes if an object doesn’t conform to the expected behavior.
  • Lack of explicitness: You might make your code less explicit and more challenging to understand. The lack of explicit interface definitions might make it more difficult to grasp what behavior an object must exhibit.
  • Potential maintenance issues: You might have issues tracking which objects must exhibit certain behaviors. Behavior changes in certain objects may impact other parts of the code, making it harder to maintain, reason about, and debug.

You should weigh these pros and cons when you consider using duck typing in your code. Depending on the context and requirements of your project, duck typing can provide flexibility, but it might also introduce potential issues that may need careful evaluation.

Exploring Duck Typing in Python’s Built-in Tools

Duck typing is a core concept in Python, and it’s present in core components of the language. It’s an approach to typing that makes Python a highly flexible language that doesn’t rely on rigid type checks but on behaviors and functionalities.

There are many examples of supporting and using duck typing in Python. One of the most well-known is the fact that built-in types, such as lists, tuples, strings, and dictionaries, support operations like iteration, sorting, and reversing.

Because duck typing is all about the behaviors of objects, you’ll find that there are general behaviors that can be useful in more than one type. When it comes to built-in types, such as lists, tuples, strings, dictionaries, and sets, you’ll soon realize that all of them support iteration. You can use them directly in your for loops:

Python
>>> numbers = [1, 2, 3]
>>> person = ("Jane", 25, "Python Dev")
>>> letters = "abc"
>>> ordinals = {"one": "first", "two": "second", "three": "third"}
>>> even_digits = {2, 4, 6, 8}
>>> collections = [numbers, person, letters, ordinals, even_digits]

>>> for collection in collections:
...     for value in collection:
...         print(value)
...
1
2
3
Jane
25
Python Dev
a
b
c
one
two
three
8
2
4
6

In this code snippet, you define a few variables that hold different built-in collection types. Then, you start a for loop over the collections and an inner loop over the data in each collection. Even though the built-in types are significantly different from one another, all of them support iteration.

Here’s a summary of some general operations that you can run on built-in collections:

Operation Lists Tuples Strings Ranges Dictionaries Sets
Iteration
Indexing
Slicing
Concatenating
Finding length
Reversing
Sorting

The operations listed in this table are just a sample of all the use cases where Python supports and takes advantage of duck typing. You’ll find several more examples as you dive deeper into the language, especially if you look at Python’s built-in functions, which represent everyday operations.

Supporting Duck Typing in Custom Classes

Up to this point, you’ve learned that Python extensively uses duck typing in built-in types. You can support duck typing in your custom classes using two different approaches:

  1. Regular methods
  2. Special methods

In the following sections, you’ll learn how to support duck typing in your own classes using the approaches listed above.

Using Regular Methods

You’ve already seen a toy example of how to support duck typing through regular methods. For a more elaborate example, say that you want to create classes to read different file formats. You need classes for reading text, CSV, and JSON files.

Click the collapsible section below to get the sample content of each file:

Text file.txt
John
25
Engineer
Jane
22
Designer
CSV file.csv
name,age,job
John,25,Engineer
Jane,22,Designer
JSON file.json
[
    {
        "name": "John",
        "age": 25,
        "job": "Engineer"
    },
    {
        "name": "Jane",
        "age": 22,
        "job": "Designer"
    }
]

You can take advantage of duck typing by providing the required behavior in every class. Here’s a quick implementation of your classes:

Python readers.py
import csv
import json
from itertools import batched  # Python >= 3.12

class TextReader:
    def __init__(self, filename):
        self.filename = filename

    def read(self):
        with open(self.filename, encoding="utf-8") as file:
            return [
                {
                    "name": batch[0].strip(),
                    "age": batch[1].strip(),
                    "job": batch[2].strip(),
                }
                for batch in batched(file.readlines(), 3)
            ]

class CSVReader:
    def __init__(self, filename):
        self.filename = filename

    def read(self):
        with open(self.filename, encoding="utf-8", newline="") as file:
            return list(csv.DictReader(file))

class JSONReader:
    def __init__(self, filename):
        self.filename = filename

    def read(self):
        with open(self.filename, encoding="utf-8") as file:
            return json.load(file)

In this example, you have the TextReader, CSVReader, and JSONReader classes. All these classes have a .filename attribute and a .read() method. Your classes share a common interface. This characteristic makes them support duck typing. So, you can use them interchangeably:

Python
>>> from readers import TextReader, CSVReader, JSONReader

>>> readers = [
...     TextReader("file.txt"),
...     CSVReader("file.csv"),
...     JSONReader("file.json"),
... ]

>>> for reader in readers:
...     print(reader.read())
...
[
    {'name': 'John', 'age': '25', 'job': 'Engineer'},
    {'name': 'Jane', 'age': '22', 'job': 'Designer'}
]
[
    {'name': 'John', 'age': '25', 'job': 'Engineer'},
    {'name': 'Jane', 'age': '22', 'job': 'Designer'}
]
[
    {'name': 'John', 'age': 25, 'job': 'Engineer'},
    {'name': 'Jane', 'age': 22, 'job': 'Designer'}
]

In this example, you run a for loop over the three reader objects. Each object points to a specific file with the appropriate format. Because all the classes have the .read() method, you can use them interchangeably, and your code will work correctly.

Thanks to duck typing, you don’t have to write multiple versions of your client code. This way, your code will be flexible and scalable.

Using Special Methods and Protocols

The second approach to supporting duck typing in your custom classes is to use special methods and protocols. Special methods are those whose names start and end with a double underscore. These methods have special meanings to Python. They’re a fundamental part of Python’s object-oriented infrastructure.

Protocols are sets of special methods that support specific features of the language, such as the iterator, context manager, and sequence protocols. Protocols are informal interfaces that are defined in the documentation.

Following established protocols improves your chances of leveraging existing standard-library and third-party code, thanks to duck typing.

To illustrate how you can support duck typing through special methods and protocols, say that you need to write a class that implements a queue data structure. You need your queue to be iterable and support the built-in len() and reversed() functions. It should also support membership tests with the in operator.

Here’s a possible implementation of your class:

Python queues.py
from collections import deque

class Queue:
    def __init__(self):
        self._elements = deque()

    def enqueue(self, element):
        self._elements.append(element)

    def dequeue(self):
        return self._elements.popleft()

    def __iter__(self):
        return iter(self._elements)

    def __len__(self):
        return len(self._elements)

    def __reversed__(self):
        return reversed(self._elements)

    def __contains__(self, element):
        return element in self._elements

Your Queue class uses a deque object to store the data. The deque class from the collections module allows you to create efficient double-ended queues.

The Queue class has the classic queue operations enqueue and dequeue to append elements to the end of the queue and remove elements from the beginning of the queue, respectively.

Next, you have the .__iter__() special method, which allows you to support iterations. Note that this method uses the built-in iter() function to return an iterator over the elements in the queue. With this method, you ensure that your class works in for loops, comprehensions, and similar constructs.

Then, you have the .__len__() and .__reverse__() methods. They allow you to support the built-in len() and reversed() functions. Finally, you have .__contains__(), which you need to support membership tests.

Here’s how your class works in practice:

Python
>>> from queues import Queue

>>> queue = Queue()
>>> queue.enqueue(1)
>>> queue.enqueue(2)
>>> queue.enqueue(3)

>>> [item for item in queue]
[1, 2, 3]

>>> len(queue)
3
>>> list(reversed(queue))
[3, 2, 1]

>>> 2 in queue
True
>>> 6 in queue
False

In this example, you first populate the queue object with three elements. To confirm that your queue is iterable, you run a quick list comprehension that iterates over the items in the queue.

Then, you call the len() function with your queue as an argument to get the number of elements in the queue. Next, you use the reversed() function to get a reversed iterator over the element in the queue. Here, you use the list() constructor to consume the iterator and display the content of your queue as a list.

Finally, you use the queue object in membership tests with the in operator. This way, you’ve confirmed that the class supports all the required operations. That’s the magic of duck typing through special methods and protocols.

Exploring Alternatives to Duck Typing

In practice, you’ll find use cases where duck typing isn’t the right approach to solving a given problem. In some cases, you may need a more explicit interface, or maybe you want to make sure that you won’t get runtime errors or have maintenance issues.

When explicit interface definition is preferred over duck typing, then you should use something different. In the following sections, you’ll learn about a couple of alternatives to duck typing. To kick things off, you’ll start with abstract base classes.

Using Abstract Base Classes

Abstract base classes (ABCs) define a specific set of public methods and attributes (API) that all their subclasses must implement. They provide an excellent replacement for duck typing. They’re the recommended approach when you need additional protections and guarantees around the interface that your classes must adhere to.

To define an abstract base class in Python, you can use the abc module from the standard library. This module provides a couple of ABC-related tools that you can use for the job.

To illustrate how to use ABCs instead of duck typing, say that you want to create classes to represent different vehicles. The classes need to have the .start(), .stop(), and .drive() methods.

Using duck typing, you can write the classes like in the following code:

Python vehicles_duck.py
class Car:
    def __init__(self, make, model, color):
        self.make = make
        self.model = model
        self.color = color

    def start(self):
        print("The car is starting")

    def stop(self):
        print("The car is stopping")

    def drive(self):
        print("The car is driving")

class Truck:
    def __init__(self, make, model, color):
        self.make = make
        self.model = model
        self.color = color

    def start(self):
        print("The truck is starting")

    def stop(self):
        print("The truck is stopping")

    def drive(self):
        print("The truck is driving")

These classes share the expected interface. They’re independent of each other and decoupled. They don’t need each other to work correctly. So, you can use them interchangeably in a duck typing context:

Python
>>> from vehicles_duck import Car, Truck

>>> vehicles = [
...     Car("Ford", "Mustang", "Red"),
...     Truck("Ford", "F-150", "Blue"),
... ]

>>> for vehicle in vehicles:
...     vehicle.start()
...     vehicle.drive()
...     vehicle.stop()
...
The car is starting
The car is driving
The car is stopping
The truck is starting
The truck is driving
The truck is stopping

Your classes work correctly because they share the same behaviors. They support duck typing.

What if you want to create a Jeep class that also works in this loop? In that case, you need to know the interface that Jeep must implement. Knowing the interface will require reviewing the code of Car and Truck or its documentation.

In this short example, the task is relatively simple. However, when your classes are complex, and their interfaces don’t match completely, you will need to invest quite a bit of time in determining the correct interface for a new class like Jeep.

Alternatively, you can create an ABC called Vehicle that defines the required interface and makes all your vehicle classes inherit from it. With this strategy, you’ll rely on the base class to enforce the necessary interface. The ABC will let you know the correct interface quickly.

Here’s how you can do this:

Python vehicles_abc.py
from abc import ABC, abstractmethod

class Vehicle(ABC):
    def __init__(self, make, model, color):
        self.make = make
        self.model = model
        self.color = color

    @abstractmethod
    def start(self):
        raise NotImplementedError("start() must be implemented")

    @abstractmethod
    def stop(self):
        raise NotImplementedError("stop() must be implemented")

    @abstractmethod
    def drive(self):
        raise NotImplementedError("drive() must be implemented")

In this code, you’ve created a Vehicle class that inherits from abc.ABC. In Vehicle, you define the .__init__() method to initialize the required attributes. Moving this method to the base class saves you from repetitive code in subclasses.

Then, you define the required methods as abstract methods using the @abstractmethod decorator. Note that the abstract methods don’t provide a specific implementation. They just raise NotImplementedError exceptions.

You can’t instantiate an ABC directly. They work as an API template for other classes to adhere to:

Python
>>> from vehicles_abc import Vehicle

>>> Vehicle("Ford", "Mustang", "Red")
Traceback (most recent call last):
    ...
TypeError: Can't instantiate abstract class Vehicle
    with abstract methods 'drive', 'start', 'stop'

When you try to instantiate an abstract class like Vehicle, you get a TypeError exception. The exception message makes that clear. So, you can only subclass an ABC.

You have a base class that enforces a given API in its subclasses. You can’t do this with duck typing. Now, you create specific vehicles by inheriting from Vehicle and implementing all the required methods:

Python vehicles_abc.py
# ...

class Car(Vehicle):
    def start(self):
        print("The car is starting")

    def stop(self):
        print("The car is stopping")

    def drive(self):
        print("The car is driving")

class Truck(Vehicle):
    def start(self):
        print("The truck is starting")

    def stop(self):
        print("The truck is stopping")

    def drive(self):
        print("The truck is driving")

In this code snippet, you rewrite the Car and Truck classes by inheriting from Vehicle. Both implement the required methods. They don’t need to have their own .__init__() method because they inherit this method from Vehicle.

That’s it! You’ve replaced duck typing with ABCs. Note that in this case, your classes are coupled with the base class. This can be a limitation because you won’t be able to reuse one of your classes in a different project unless you also take the Vehicle class with you.

Finally, what happens if you try to create a Jeep class that inherits from Vehicle without providing the required interface? The code below lets you know the answer:

Python
>>> from vehicles_abc import Vehicle

>>> class Jeep(Vehicle):
...     def start(self):
...         print("The jeep is starting")
...

>>> jeep = Jeep("Land Rover", "Defender", "Black")
Traceback (most recent call last):
    ...
TypeError: Can't instantiate abstract class Jeep
    with abstract methods 'drive', 'stop'

In this example, you try to create a Jeep class by inheriting from Vehicle. You only implement the .start() method, which doesn’t fulfill the complete interface. When you try to instantiate this class, you get a TypeError because the required interface is incomplete. This behavior is part of how ABCs enforce specific interfaces.

Checking Types Explicitly

Checking the type of an object explicitly is another alternative to duck typing. This alternative is more restrictive and consists of making sure that an object is of a given type or has the required methods before you can use it in your code.

In Python, to check whether an object comes from a given class, you can use the built-in isinstance() function. To check whether an object has a specific method or attribute, you can use the built-in hasattr() function.

To illustrate how these tools work, say that you have the following toy classes:

Python birds_v2.py
class Duck:
    def fly(self):
        print("The duck is flying")

    def swim(self):
        print("The duck is swimming")

class Pigeon:
    def fly(self):
        print("The pigeon is flying")

These classes partially support duck typing. They work interchangeably if you only use the .fly() method. When you need the .swim() method, then Pigeon will fail with an AttributeError:

Python
>>> from birds_v2 import Duck, Pigeon

>>> birds = [Duck(), Pigeon()]

>>> for bird in birds:
...     bird.fly()
...
The duck is flying
The pigeon is flying

>>> for bird in birds:
...     bird.swim()
...
The duck is swimming
Traceback (most recent call last):
    ...
AttributeError: 'Pigeon' object has no attribute 'swim'

In this example, the first loop works because both objects have the .fly() method. In contrast, the second loop fails because Pigeon doesn’t have the .swim() method.

To avoid the failure described above, you can check the object’s type before calling the .swim() method:

Python
>>> for bird in birds:
...     if isinstance(bird, Duck):
...         bird.swim()
...
The duck is swimming

In this example, you use the built-in isinstance() function to check whether the current object is an instance of Duck, the class that provides the .swim() method.

Alternatively, you can explicitly check if the current object has the desired method using the hasattr() function:

Python
>>> for bird in birds:
...     if hasattr(bird, "swim"):
...         bird.swim()
...
The duck is swimming

The hasattr() function allows for a more generic approach than isinstance(). In this case, you can check for specific behaviors rather than types, which may be convenient in some situations.

Even though checking the type of an object before using it can be a good solution in some cases, it’s not the best approach. One of Python’s main characteristics is its flexibility regarding types, and checking types all the time isn’t a very flexible thing to do.

Using Duck Typing and Type Hints

As you’ve learned throughout this tutorial, duck typing is a type system widely used in Python. It adds flexibility to the language, and it’s a pretty common system that you’ll find in built-in types, the standard library, and third-party code.

However, sometimes it may be challenging to combine duck typing and type hints, which is Python’s way to provide type context.

It’s great that you can write a single function that can process different types of objects, provided that they support the required behavior. The hard part can be to communicate intent through type hints. In the following sections, you’ll dive into a few concepts that can help you better understand the challenge and figure out how you can solve it.

Understanding Type Hints and Static Duck Typing

Type hints are the Pythonic way to express the type of objects in a piece of code. Python’s type hints have significantly evolved in the last few years. Type checking has become more and more popular in Python code. Most code editors and IDEs (integrated development environments) have integrated type checkers that use type hints to detect possible errors in your code.

Using type hints in your code has several associated advantages. Among other benefits, you’ll have the following:

  • Preventing type-related bugs
  • Powering automatic type checkers
  • Supporting auto-completion in editors
  • Providing code documentation
  • Allowing for data validation
  • Allowing for data serialization

In the early stages of Python’s type hints, the system was mainly nominal. In a nominal type system, a class can replace another class if the latter is a subclass of the former. This approach represented a challenge for duck typing, which doesn’t rely on types but on behaviors.

The type-hinting system continued to evolve, and now it also supports a structural type system. In this kind of system, a class can replace another if both have the same structure. For example, you can use the len() function with all the classes that define the .__len__() method. This method is part of the class’s internal structure.

This is known as structural subtyping or static duck typing. It’s how Python has managed to make type hints suitable for duck typing. Protocols and abstract base classes are core to achieving this compatibility.

Using Protocols and ABCs

Python 3.8 introduced protocols in the type-hinting system. A protocol specifies one or more methods that a class must implement to support a given feature. So, protocols have to do with a class’s internal structure. You’ve already heard about common protocols, such as iterator, context manager, and sequence protocols.

Protocols and abstract base classes fill the gap between duck typing and type hints. They help type checkers catch type-related issues and potential errors, which contribute to making your code more robust.

As an example of how duck typing can collide with type hints, say that you have the following function:

Python
>>> def mean(grades: list) -> float:
...     return sum(grades) / len(grades)
...

>>> mean([4, 3, 3, 4, 5])
3.8
>>> mean((4, 3, 3, 4, 5))
3.8

This function works correctly. However, the type hint for the grades argument is limited to list objects and collides head-on with duck typing. Note that the function works with lists and tuples of numbers. It can even work with set objects.

How can you type-hint this function in a way that makes it suitable for duck typing? You can use a union type expression like in the code below:

Python
def mean(grades: list | tuple | set) -> float:
    return sum(grades) / len(grades)

This type hint works. However, it’s cumbersome. To simplify the type hint, you can take advantage of a more generic solution. It can be an ABC that defines the required interface, which consists of supporting iteration and the len() function.

In this example, the input object should be iterable and support len(). In the collections.abc module, you have an abstract base class called Collection that defines both behaviors. Using this class, you can type hint your function as in the code below:

Python
from collections.abc import Collection

def mean(grades: Collection) -> float:
    return sum(grades) / len(grades)

This type hint is more concise. If you take a look at the documentation of Collection, you’ll note that this ABC supports the .__iter__() and .__len__() methods. This way, your type checker knows that the input object must support these protocols rather than being of a specific type.

You’ll find several abstract base classes that you can use to provide type hints that are suitable for duck typing. Instead of defining types, these classes define interfaces, which are the basis on which duck typing stands.

Creating Custom Protocol Objects

In some situations, you may need to create custom classes to define your own protocols in code that relies on duck typing. To do this, you can use the Protocol class from the typing module. This class is the base for defining protocols.

To illustrate, say that you want to create a group of shape classes. You need all the classes to have .area() and .perimeter() methods:

Python shapes.py
from math import pi

class Circle:
    def __init__(self, radius: float) -> None:
        self.radius = radius

    def area(self) -> float:
        return pi * self.radius**2

    def perimeter(self) -> float:
        return 2 * pi * self.radius

class Square:
    def __init__(self, side: float) -> None:
        self.side = side

    def area(self) -> float:
        return self.side**2

    def perimeter(self) -> float:
        return 4 * self.side

class Rectangle:
    def __init__(self, length: float, width: float) -> None:
        self.length = length
        self.width = width

    def area(self) -> float:
        return self.length * self.width

    def perimeter(self) -> float:
        return 2 * (self.length + self.width)

All these classes have the required methods, so they support duck typing. They also use type hints to describe the types of input arguments and the return values of every method. Up to this point, your type checker will be happy.

Now, say that you want to write a function that takes a shape as an argument and create a description of the input shape:

Python shapes.py
# ...

def describe_shape(shape):
    print(f"{type(shape).__name__}")
    print(f" Area: {shape.area():.2f}")
    print(f" Perimeter: {shape.perimeter():.2f}")

This function takes a shape as an argument and prints a report that includes the shape name, area, and perimeter. Here’s how this function works with the different shapes:

Python
>>> from shapes import Circle, Rectangle, Square, describe_shape

>>> describe_shape(Circle(3))
Circle
 Area: 28.27
 Perimeter: 18.85

>>> describe_shape(Square(5))
Square
 Area: 25.00
 Perimeter: 20.00

>>> describe_shape(Rectangle(4, 5))
Rectangle
 Area: 20.00
 Perimeter: 18.00

Because your shapes classes support duck typing, the describe_shape() function works as expected no matter which shape you use as an argument. Now, how can you add type hints to describe_shape()? How can you make sure the input shape will have the required interface?

In this case, you can create a Protocol subclass to describe the required set of methods for your shapes:

Python shapes.py
from math import pi
from typing import Protocol

class Shape(Protocol):
    def area(self) -> float: ...

    def perimeter(self) -> float: ...

# ...

This class inherits from Protocol and defines the required methods. Note that the methods don’t have a proper implementation. They just use an ellipsis to define the protocol’s body. Alternatively, they can use a pass statement as another way to express that these methods don’t do anything. They just define a custom protocol.

Now you can use the Shape class to type-hint your describe_shape() function as in the code below:

Python shapes.py
# ...

def describe_shape(shape: Shape) -> None:
    print(f"{type(shape).__name__}")
    print(f" Area: {shape.area():.2f}")
    print(f" Perimeter: {shape.perimeter():.2f}")

In this updated version of describe_shape(), you use the Shape protocol to provide an appropriate type hint for the shape argument. With this update, your type checker will be happy because it’ll know that the argument supports the required methods.

Conclusion

Now you know that Python uses duck typing in many language constructs. This type of polymorphism doesn’t rely on inheritance. It only depends on the public methods and attributes (API) of objects. Many Python built-in classes and tools support this type system, which adds flexibility and power to the language.

In this tutorial, you’ve learned:

  • What duck typing is and what its pros and cons are
  • How Python’s classes and tools take advantage of duck typing
  • How special methods and protocols support duck typing
  • What alternatives to duck typing you’ll have in Python

Learning about duck typing will help you better understand how Python works. It can also help you write more Pythonic, flexible, and decoupled code.

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Leodanis Pozo Ramos

Leodanis is an industrial engineer who loves Python and software development. He's a self-taught Python developer with 6+ years of experience. He's an avid technical writer with a growing number of articles published on Real Python and other sites.

» More about Leodanis

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!