Loading video player…

Comparing Duck Typing in Python's Built-in Types

00:00 Exploring Duck Typing in Python’s Built-in Types. Duck typing keeps Python flexible by focusing on what an object can do instead of needing strict types.

00:11 Also, built-in types share common behaviors like iteration, sorting, and reversing, making them straightforward to use in for loops and other operations.

00:21 Now, what does this mean? It means that you can take advantage of duck typing when you’re dealing with built-in types.

00:29 Let’s go and play around with the built-in types a little. Take iteration, for example. Do you think all built-in types support iteration?

00:39 Look at this code snippet. You have a bunch of examples to test this idea. A list, a tuple, a string, a dictionary, and a set. And then you’re putting everything in a list called collections, and you’re trying to print them in a nested for loop. Or in other words, you’re testing if they support iteration.

00:57 What do you think will happen if you run this code? Would it run without any problems, or would you get an error? You can pause this video for a little and think about it.

01:09 Okay, ready? When you run the code, it works. No errors. You get every single item inside of the stuff inside your collections list, like the list, the string, and the set printed.

01:22 This means that all of the built-in types you have here support iteration. How can you connect this conclusion with what you already know about duck typing?

01:31 Tell us in the comment section below.

01:36 Now, here’s a table that breaks down which built-in Python types, like lists, tuples, strings, support different operations. You can see that some, like lists, can do almost everything, while others, like sets, don’t support indexing or slicing because they’re unordered.

01:53 This is a great example of how Python focuses on what an object can do rather than what it is. If something supports an operation, you can use it no matter the type.

02:05 Now, here’s a challenge for you. Try creating a class that checks whether a built-in type supports each operation from the table in the last slide. Configure it however you prefer, and when you’re done, show off your solution in the comment section below.

02:20 We’re excited to see what you’ll come up with.

Avatar image for Odysseas Kouvelis

Odysseas Kouvelis on July 4, 2025

Answer:

“All the aforementioned built-in types — lists, tuples, dictionaries, strings, and sets — conform to the collections.abc.Iterable protocol, which requires the implementation of the __iter__() method that returns an object adhering to the collections.abc.Iterator interface.”

Avatar image for Stephen Gruppetta

Stephen Gruppetta on July 5, 2025

Note, you say that these conform to Iterator but if they have .__iter__() then that means they are iterables, and therefore conform to Iterable.

Iterators are a different albeit related data type that must also have the .__next__() special method

Avatar image for Odysseas Kouvelis

Odysseas Kouvelis on July 5, 2025

Just for clarification:

>>> from collections.abc import Iterable, Iterator
>>> from typing import Any

>>> obj: Iterable[Any] = ...  # instantiation logic
>>> iter_: Iterator = obj.__iter__()
>>> hasattr(iter_, '__iter__')
True
>>> hasattr(iter_, '__next__')
True

The return value of obj.__iter__() must be an object conforming to the collections.abc.Iterator interface, i.e. it must implement both __iter__() (returning self) and __next__().

So to restate, as per the Iterator protocol in Python docs:

“The __iter__() method must return the iterator object itself.”

Avatar image for Stephen Gruppetta

Stephen Gruppetta on July 6, 2025

I had missed the word “returns” in your comment. Apologies. Your original statement (now I read it properly) is correct. You can always create an iterator from an iterable, which is what makes it iterable!

Avatar image for Odysseas Kouvelis

Odysseas Kouvelis on July 8, 2025

Version-Specific Behavior Detected

While working through this exercise, I noticed that my results diverged from the provided reference table for certain operations, specifically for:

  1. reversed(dict)
  2. sorted(set), sorted(range), and sorted(dict)

Operation reversed(dict) is supported in Python ≥ 3.8

In Python 3.8 and later, dictionaries implement the __reversed__() method. This means:

>>> reversed({"a": 1, "b": 2})
<dict_reversekeyiterator object at 0x7aee027c89f0>

Here, a <dict_reversekeyiterator> object that iterates over keys in reverse insertion order is returned. In earlier Python versions, this would have raised a TypeError.

Operation sorted() is broadly supported.

The following operations are all valid in modern Python (I am currently using 3.11):

>>> sorted(set([3, 1, 2]))
[1, 2, 3]
>>> sorted(range(3))
[0, 1, 2]
>>> sorted({"b": 2, "a": 1})
['a', 'b']

This works because sorted() only requires:

  • The object to be iterable.
  • The elements to support comparison via < (i.e., __lt__).

So, even types that do not formally implement a Sortable interface (which doesn’t exist in collections.abc) can still support sorted() at runtime.

This raises an important question: How do we define that the operation of sorting is supported?

I suspect such discrepancies are due to version differences, although I cannot pinpoint the exact version number where this behavior first appeared.

Avatar image for Odysseas Kouvelis

Odysseas Kouvelis on July 12, 2025

My mathematical background initially prompted me to seek a solution via issubclass(), applied upon collections.abc interfaces such as Sized, Reversible, and Iterable, in combination with hasattr() checks for special dunder methods like __getitem__ and __add__. The intention was to achieve the formality and generality of a proof, thereby avoiding any instantiation and operating solely on the class level.

Upon reflecting on the pedagogical purpose of this challenge, I decided to drop this approach in favor of displaying behavioral polymorphism—a term that refers to objects, not types. That is, the solution relies on observing whether an operation succeeds on an actual object, embracing the runtime nature of Python’s duck typing.

Lastly, the experienced reader may notice that the property-defining code repeats itself, and could be rearranged using a private helper or decorator. However, the contextual pedagogy of this exercise constrained me to avoid adopting a more compact style. “Explicit is better than implicit,…” etc.

#!/usr/bin/python3.11
"""
./getting_to_know_duck_typing_in_python/operations_supported.py

For a set of built-in Python types, determine whether they support 
common object protocols and operations, such as iteration, indexing, 
slicing, concatenation, length retrieval, reversing, and sorting.

This script illustrates Python's dynamic and duck-typed behavior by
attempting to perform these operations on concrete instances of
selected built-in types, and recording which operations succeed.

Note
----
From a mathematical standpoint, this process does not constitute a 
formal proof of support, as instantiation inherently limits generality.
Specifically, attempting operations on default-constructed instances 
(i.e., with no arguments) represents an extreme and often minimal case.

Nevertheless, such an approach is adopted here for pedagogical clarity 
and practical inspection, enabling empirical observation of behavior
while respecting Python’s runtime-driven model of polymorphism.
"""

from typing import Any


class Table:
    """
    Table for evaluating support for common operations across built-in 
    Python types.

    The objective of this exercise is to recreate the values shown in
    the tutorial's reference table. To do so, each built-in type is 
    instantiated with default arguments, and operations are applied 
    directly to the resulting object. While this does not cover all 
    possible instantiations or edge cases, it offers a practical and 
    reproducible means of observing runtime behavior.

    This approach aligns with Python's duck typing philosophy by 
    emphasizing behavior over static type declarations.

    Attributes
    ----------
    built_in_types : list[Any]
        List of initialized instances of built-in types to evaluate.
    """

    def __init__(self) -> None:
        """
        Initialize built-in type instances using zero-argument 
        constructors.

        Empty instantiation is used to enable consistent, 
        side-effect-free application of protocol operations without 
        requiring parameter handling.

        Note
        ----
        Since the `range` constructor requires at least one argument 
        (`stop`), object `range(0)` is used as a minimal, valid 
        instantiation. 
        """
        self.built_in_types: list[Any] = [
            list(), tuple(), str(), range(0), set(), dict()
        ]

    @property
    def iteration(self) -> list[bool]:
        """
        Check whether each object supports iteration via `iter(obj)`.

        Returns
        -------
        list[bool]
            True if the object is iterable; False otherwise.
        """
        results: list[bool] = []
        for obj in self.built_in_types:
            try:
                _: Any = iter(obj)
                results.append(True)
            except Exception:
                results.append(False)
        return results

    @property
    def indexing(self) -> list[bool]:
        """
        Check whether `obj[0]` is valid for each object.

        Returns
        -------
        list[bool]
            True if the object allows positional indexing; False 
            otherwise.

        Notes
        -----
        IndexError is interpreted as valid indexing on an empty 
        structure.
        """
        results: list[bool] = []
        for obj in self.built_in_types:
            try:
                _: Any = obj[0]
                results.append(True)
            except IndexError:
                results.append(True)
            except (KeyError, TypeError):
                results.append(False)
        return results

    @property
    def slicing(self) -> list[bool]:
        """
        Check whether `obj[:]` is valid for each object.

        Returns
        -------
        list[bool]
            True if the object supports slicing; False otherwise.
        """
        results: list[bool] = []
        for obj in self.built_in_types:
            try:
                _: Any = obj[:]
                results.append(True)
            except TypeError:
                results.append(False)
        return results

    @property
    def concatenating(self) -> list[bool]:
        """
        Check whether `obj + obj` is supported for each object.

        Returns
        -------
        list[bool]
            True if concatenation is supported; False otherwise.
        """
        results: list[bool] = []
        for obj in self.built_in_types:
            try:
                _: Any = obj + obj
                results.append(True)
            except TypeError:
                results.append(False)
        return results

    @property
    def finding_length(self) -> list[bool]:
        """
        Check whether `len(obj)` is valid for each object.

        Returns
        -------
        list[bool]
            True if the object supports length retrieval; False 
            otherwise.
        """
        results: list[bool] = []
        for obj in self.built_in_types:
            try:
                _: Any = len(obj)
                results.append(True)
            except Exception:
                results.append(False)
        return results

    @property
    def reversing(self) -> list[bool]:
        """
        Check whether `reversed(obj)` is valid for each object.

        Returns
        -------
        list[bool]
            True if the object supports reverse iteration; False 
            otherwise.
        """
        results: list[bool] = []
        for obj in self.built_in_types:
            try:
                _: Any = reversed(obj)
                results.append(True)
            except Exception:
                results.append(False)
        return results

    @property
    def sorting(self) -> list[bool]:
        """
        Check whether `sorted(obj)` is valid for each object.

        Returns
        -------
        list[bool]
            True if the object can be sorted; False otherwise.
        """
        results: list[bool] = []
        for obj in self.built_in_types:
            try:
                _: Any = sorted(obj)
                results.append(True)
            except Exception:
                results.append(False)
        return results


def main() -> None:
    """
    Render an operation support table for selected built-in types.

    The output displays which operations succeed on each type by 
    applying them to default-constructed instances and reporting results
    in a visually aligned format.

    Note
    ----
    The output is not version-agnostic; results may vary across Python 
    versions. The current results were obtained using Python 3.11, as 
    indicated in the shebang. This explains any divergence from the 
    tutorial's reference output.
    """
    table: Table = Table()
    type_names: list[str] = [
        type(obj).__name__ for obj in table.built_in_types
    ]

    col_width: int = 13

    header_row: str = f"{'Operation ':<{col_width}}" + " ".join(
        f"{name:^{col_width}}" for name in type_names
    )
    print(header_row)
    print("-" * len(header_row))

    def print_row(label: str, values: list[bool]) -> None:
        """Format and print a single row of the result table."""
        row: str = f"{label:<{col_width}}" + "".join(
            f"{'✅' if val else '❌':^{col_width}}" for val in values
        )
        print(row)

    print_row("Iteration", table.iteration)
    print_row("Indexing", table.indexing)
    print_row("Slicing", table.slicing)
    print_row("Concatenating", table.concatenating)
    print_row("Length", table.finding_length)
    print_row("Reversing", table.reversing)
    print_row("Sorting", table.sorting)


if __name__ == "__main__":
    main()

Become a Member to join the conversation.