Iterable, ordered, mutable, and hashable (and their opposites) are characteristics for describing Python objects or data types. Despite being frequently used, these terms are often confused or misunderstood. In this article, we’ll discuss what each of them really means and implies, what data types they are related to, the main nuances of these properties, and some useful workarounds.
Iterable
An iterable object in Python is an object that can be looped over for extracting its items one by one or applying a certain operation on each item and returning the result. Iterables are mostly composite objects representing a collection of items (lists, tuples, sets, frozensets, dictionaries, ranges, and iterators), but also strings are iterable.
For all iterable data types, we can use a for-loop to iterate over an object:
for i in (1, 2, 3):
print(i)
Output:
1
2
3
For a Python dictionary, the iteration is performed by default over the dictionary keys:
dct = {'a': 1, 'b': 2}
print('Dictionary:', dct)
print('Iterating over the dictionary keys:')
for i in dct:
print(i)
Output:
Dictionary: {'a': 1, 'b': 2}
Iterating over the dictionary keys:
a
b
If we want to iterate over the dictionary values, we have to use the values()
method on the dictionary:
print('Iterating over the dictionary values:')
for i in dct.values():
print(i)
Output:
Iterating over the dictionary values:
1
2
If instead, we want to iterate over both the dictionary keys and values, we should use the items()
method:
print('Iterating over the dictionary keys and values:')
for k, v in dct.items():
print(k, v)
Output:
Iterating over the dictionary keys and values:
a 1
b 2
All iterable Python objects have the __iter__
attribute. Hence, the easiest way to check if a Python object is iterable or not is to use the hasattr()
method on it checking whether the __iter__
attribute is available:
print(hasattr(3.14, '__iter__'))
print(hasattr('pi', '__iter__'))
Output:
False
True
We can get an iterator object from any iterable Python object using the iter()
function on it:
lst = [1, 2, 3]
print(type(lst))
iter_obj = iter(lst)
print(type(iter_obj))
Output:
<class 'list'>
<class 'list_iterator'>
Quite expected (and a bit tautologic), an iterator object is iterable, so we can iterate over it. However, unlike all the other iterables, an iterator object gets exhausted in the process of iteration losing its elements one by one:
from operator import length_hint
lst = [1, 2, 3]
iter_obj = iter(lst)
print('The iterator object length before iteration:')
print(length_hint(iter_obj))
for i in iter_obj:
pass
print('The iterator object length after iteration:')
print(length_hint(iter_obj))
Output:
The iterator object length before iteration:
3
The iterator object length after iteration:
0
Ordered vs. Unordered
Ordered objects in Python are those iterable objects where a determined order of items is kept unless we intentionally update such objects (inserting new items, removing items, sorting items). Ordered objects are strings, lists, tuples, ranges, and dictionaries (starting from Python 3.7+), unordered – sets, frozensets, and dictionaries (in Python versions older than 3.7).
lst = ['a', 'b', 'c', 'd']
s = {'a', 'b', 'c', 'd'}
print(lst)
print(s)
Output:
['a', 'b', 'c', 'd']
{'b', 'c', 'a', 'd'}
Since the order in ordered objects is preserved, we can access and modify the object’s items by indexing or slicing:
lst = ['a', 'b', 'c', 'd']
# Access the 1st item of the list.
print(lst[0])
# Access the 2nd and 3rd items of the list.
print(lst[1:3])
# Modify the 1st item.
lst[0] = 'A'
print(lst[0])
Output:
a
['b', 'c']
A
To extract a specific value from a Python dictionary, we commonly use the corresponding key name rather than the key index in the dictionary:
dct = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
print(dct['b'])
Output:
2
However, if we, for some reason, need to extract the value of the key with a known position in the dictionary (let’s say, we know only the position and not the key itself), or a bunch of values for a slice of keys with a defined range of positions in the dictionary, we still can technically do it:
dct = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
# Access the value of the 1st key in the dictionary.
print(list(dct.values())[0])
# Access the values of the 2nd to 4th keys in the dictionary.
print(list(dct.values())[1:4])
# Access the 2nd to 4th keys in the dictionary.
print(list(dct.keys())[1:4])
Output:
1
[2, 3, 4]
['b', 'c', 'd']
The above workaround is not very straightforward. However, it helps index a Python dictionary by keys or values.
In unordered objects, we can’t access individual items:
s = {'a', 'b', 'c', 'd'}
print(s[0])
Output:
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~AppDataLocalTemp/ipykernel_9980/849534030.py in <module>
1 s = {'a', 'b', 'c', 'd'}
----> 2 print(s[0])
TypeError: 'set' object is not subscriptable
If we have a composite ordered object containing other composite objects we can dig deeper and access the inner items of that object’s items, in the case they are ordered as well. For example, if a Python list contains another Python list, we can access the items of the inner list:
lst_2 = [[1, 2, 3], {1, 2, 3}, 10]
print(lst_2[0][2])
Output:
3
However, we can’t access the items of the set in that list since Python sets are unordered:
print(lst_2[1][2])
Output:
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~AppDataLocalTemp/ipykernel_9980/895995558.py in <module>
----> 1 print(lst_2[1][2])
TypeError: 'set' object is not subscriptable
Mutable vs. Immutable
Mutable objects in Python are those objects that can be modified. Mutability doesn’t necessarily mean the ability to access individual items of a composite object by indexing or slicing. For example, a Python set is unordered and unindexed and nevertheless, it’s a mutable data type since we can modify it by adding new items or removing items from it.
On the other hand, a Python tuple is an immutable data type, but we can easily access its individual items by indexing and slicing (without being able to modify them, though). Also, it’s possible to index and slice a range object extracting from it integers or smaller ranges.
In general, mutable data types are lists, dictionaries, sets, and bytearrays, while immutable – all the primitive data types (strings, integers, floats, complex, boolean, bytes), ranges, tuples, and frozensets.
Let’s explore one interesting caveat about tuples. Being an immutable data type, a tuple can contain items of a mutable data type, e.g., lists:
tpl = ([1, 2], 'a', 'b')
print(tpl)
Output:
([1, 2], 'a', 'b')
The tuple above contains a list as its first item. We can access it but can’t re-assign another value to this item:
# Access the 1st item of the tuple.
print(tpl[0])
# Try to re-assign a new value to the 1st item of the tuple.
tpl[0] = 1
Output:
[1, 2]
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~AppDataLocalTemp/ipykernel_9980/4141878083.py in <module>
3
4 # Trying to modify the first item of the tuple
----> 5 tpl[0] = 1
TypeError: 'tuple' object does not support item assignment
However, since a Python list is a mutable and ordered data type, we can both access any of its items and modify them:
# Access the 1st item of the list.
print(tpl[0][0])
# Modify the 1st item of the list.
tpl[0][0] = 10
Output:
1
As a result, one of the items of our tuple has been changed, and the tuple itself looks different than the initial one:
print(tpl)
Output:
([10, 2], 'a', 'b')
Hashable vs. Unhashable
A hashable Python object is any object that has a hash value – an integer identificator of that object which never changes during its lifetime. To check if an object is hashable or not and find out its hash value (if it’s hashable), we use the hash()
function on this object:
print(hash(3.14))
Output:
322818021289917443
If the object is unhashable, a TypeError
will be thrown:
print(hash([1, 2]))
Output:
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~AppDataLocalTemp/ipykernel_9980/3864786969.py in <module>
----> 1 hash([1, 2])
TypeError: unhashable type: 'list'
Almost all immutable objects are hashable (we’ll see soon a particular exception) while not all hashable objects are immutable. In particular, all the primitive data types (strings, integers, floats, complex, boolean, bytes), ranges, frozensets, functions, and classes, both built-in and user-defined, are always hashable, while lists, dictionaries, sets, and bytearrays are unhashable.
A curious case is a Python tuple. Since it is an immutable data type, it should be hashable. And indeed, it seems so:
print(hash((1, 2, 3)))
Output:
529344067295497451
Notice also that if we assign this tuple to a variable and then run the hash()
function on that variable, we’ll obtain the same hash value:
a_tuple = (1, 2, 3)
print(hash(a_tuple))
Output:
529344067295497451
What if a tuple contains mutable items, just like the one we saw in the previous section?
tpl = ([1, 2], 'a', 'b')
print(hash(tpl))
Output:
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~AppDataLocalTemp/ipykernel_8088/3629296315.py in <module>
1 tpl = ([1, 2], 'a', 'b')
----> 2 print(hash(tpl))
TypeError: unhashable type: 'list'
We see that Python tuples can be either hashable or unhashable. They are unhashable only if they contain at least one mutable item.
Interesting that even unhashable tuples, like the one above, have the __hash__
attribute:
# Check if a hashable tuple has the '__hash__' attribute.
print(hasattr((1, 2, 3), '__hash__'))
# Check if an unhashable tuple has the '__hash__' attribute.
print(hasattr(([1, 2], 'a', 'b'), '__hash__'))
Output:
True
True
We mentioned earlier that not all hashable objects are immutable. An example of such a case is user-defined classes that are mutable but hashable. Also, all the instances of a class are hashable and have the same hash value as the class itself:
class MyClass:
pass
x = MyClass
print(hash(MyClass))
print(hash(x))
Output:
170740243488
170740243488
In general, when two Python objects are equal, their hash values are also equal:
# Check the equal objects and their hash values.
print(True==1)
print(hash(True)==hash(1))
# Check the unequal objects and their hash values.
print('god'=='dog')
print(hash('god')==hash('dog'))
Output:
True
True
False
False
Conclusion
To sum up, we explored the frequently-used but often misunderstood Python object and data type characteristics, such as iterable, ordered, mutable, hashable, and their opposites, from many sides and in detail, including some particular cases and exceptions.
Thanks for reading!
You can find interesting also these articles:
16 Underrated Pandas Series Methods And When To Use Them