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.
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
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.”
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!
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:
reversed(dict)
sorted(set)
,sorted(range)
, andsorted(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.
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.
Odysseas Kouvelis on July 4, 2025
Answer: