Skip to content

Latest commit

 

History

History
6012 lines (5250 loc) · 296 KB

File metadata and controls

6012 lines (5250 loc) · 296 KB

Collection Datatypes

The object Base Class and Collections Abstract Base Class

Recall from an earlier tutorial covering the Python data model that the object class is the base class of all classes. dir can be used to view a list of its identifiers:

In [1]: dir(object)
Out[1]: ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', 
         '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', 
         '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', 
         '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', 
         '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

The identifiers can also be viewed if object is input followed by a dot .:

In [2]: object.
# -------------------------------
# Available Identifiers for `object`:
# -------------------------------------
#   🔧 Functions:
#     - __init__(self, /, *args, **kwargs)          : Initializes the object.
#     - __new__(*args, **kwargs)                    : Creates a new instance of the class.
#     - __delattr__(self, name, /)                  : Defines behavior for when an attribute is deleted.
#     - __dir__(self, /)                            : Default dir() implementation.
#     - __sizeof__(self, /)                         : Returns the size of the object in memory, in bytes.
#     - __eq__(self, value, /)                      : Checks for equality with another object.
#     - __ne__(self, value, /)                      : Checks for inequality with another object.
#     - __lt__(self, value, /)                      : Checks if the object is less than another.
#     - __le__(self, value, /)                      : Checks if the object is less than or equal to another.
#     - __gt__(self, value, /)                      : Checks if the object is greater than another.
#     - __ge__(self, value, /)                      : Checks if the object is greater than or equal to another.
#     - __repr__(self, /)                           : Returns a string representation of the object.
#     - __str__(self, /)                            : Returns a string for display purposes.
#     - __format__(self, format_spec, /)            : Returns a formatted string representation of the object.
#     - __hash__(self, /)                           : Returns a hash of the object.
#     - __getattribute__(self, name, /)             : Gets an attribute from the object.
#     - __setattr__(self, name, value, /)           : Sets an attribute on the object.
#     - __delattr__(self, name, /)                  : Deletes an attribute from the object.
#     - __reduce__(self, /)                         : Prepares the object for pickling.
#     - __reduce_ex__(self, protocol, /)            : Similar to __reduce__, with a protocol argument.
#     - __init_subclass__(...)                      : Called when a class is subclassed; default 
#                                                     implementation does nothing.
#     - __subclasshook__(...)                       : Customize issubclass() for abstract classes.
#
#    🔍 Attributes:
#     - __class__                                    : The class of the object.
#     - __doc__                                      : The docstring of the object.
# -------------------------------------

If the tuple class is now examined:

In [2]: dir(tuple)
Out[2]: ['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__', 
         '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', 
         '__getitem__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', 
         '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', 
         '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', 
         '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 
         'count', 'index']

Notice the high level of consistency with the identifiers in the str and the bytes classes, which was covered in the previous tutorial:

In [3]: dir(str)
Out[3]: ['__add__', '__class__', '__contains__', '__delattr__', '__dir__', 
         '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', 
         '__getitem__', '__getstate__', '__gt__', '__hash__', '__init__', 
         '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', 
         '__mul__', '__ne__', '__new__', '__reduce__', '__repr__', 
         '__radd__', '__rmatmul__', '__rmul__', '__setattr__', '__sizeof__', 
         '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 
         'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 
         'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 
         'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 
         'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 
         'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 
         'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase',
         'title', 'upper', 'zfill']
In [4]: dir(bytes)
Out[4]: ['__add__', '__bytes__', '__class__', '__contains__', '__delattr__', 
         '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', 
         '__getitem__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', 
         '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', 
         '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', 
         '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', 
         '__subclasshook__', 'capitalize', 'center', 'count', 'decode', 'endswith', 
         'expandtabs', 'find', 'fromhex', 'hex', 'index', 'isalnum', 'isalpha', 
         'isascii', 'isdigit', 'islower', 'isspace', 'istitle', 'isupper', 'join', 
         'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'removeprefix', 
         'removesuffix', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 
         'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 
         'swapcase', 'title', 'translate', 'upper', 'zfill']

The tuple, str and bytes classes each have the design pattern of the base class object and the abstract base classes Collection and Sequence. The abstract base class immutable Collection gives collection based properties i.e. each of these classes is a collection of some sort of element. The abstract base class immutable Sequence which gives additional ordered properties and each element corresponds to a numeric index. The tuple is a Sequence where each element in the Sequence is a reference to a Python object. The str is a Sequence where each element in the Sequence is a Unicode character and the bytes is a Sequence where each element in the Sequence is a byte, which is essentially an int between 0:256. Immutable means an instance is read only and cannot be modified after instantiation.

When tuple is input, followed by a dot . the identifiers are typically listed alphabetically. However it is easier to understand the identifiers in the tuple class when the identifiers are grouped by design pattern and purpose:

In [5]: tuple.
# -------------------------------
# Available Identifiers for `tuple`:
# -------------------------------------

# 🔧 Functions from `object` (inherited by `tuple`):
#     - __init__(self, /, *args, **kwargs)          : Initializes the object (tuple initialization).
#     - __new__(*args, **kwargs)                    : Creates a new instance of the tuple.
#     - __delattr__(self, name, /)                  : Defines behavior for when an attribute is deleted.
#     - __dir__(self, /)                            : Default dir() implementation.
#     - __sizeof__(self, /)                         : Returns the size of the object in memory, in bytes.
#     - __eq__(self, value, /)                      : Checks for equality with another object.
#     - __ne__(self, value, /)                      : Checks for inequality with another object.
#     - __lt__(self, value, /)                      : Checks if the object is less than another.
#     - __le__(self, value, /)                      : Checks if the object is less than or equal to another.
#     - __gt__(self, value, /)                      : Checks if the object is greater than another.
#     - __ge__(self, value, /)                      : Checks if the object is greater than or equal to another.
#     - __repr__(self, /)                           : Returns a string representation of the tuple.
#     - __str__(self, /)                            : Returns a string for display purposes.
#     - __format__(self, format_spec, /)            : Formats the object as a string.
#     - __hash__(self, /)                           : Returns a hash of the object.
#     - __getattribute__(self, name, /)             : Gets an attribute from the object.
#     - __setattr__(self, name, value, /)           : Sets an attribute on the object.
#     - __reduce__(self, /)                         : Prepares the object for pickling.
#     - __reduce_ex__(self, protocol, /)            : Similar to __reduce__, with a protocol argument.

# 🔍 Attributes from `object`:
#     - __class__                                   : The class of the tuple.
#     - __doc__                                     : The docstring of the tuple class.

# 🔧 Collection-Based Methods (from `tuple` and the Collection ABC):
#     - __contains__(self, key, /)                  : Checks if an element is in the tuple (`in`).
#     - __iter__(self, /)                           : Returns an iterator over the tuple.
#     - __len__(self, /)                            : Returns the number of elements in the tuple.
#     - __getitem__(self, key, /)                   : Retrieves an element by index (`[]`).
#     - count(self, value, /)                       : Counts the number of occurrences of a value.
#     - index(self, value, start=0, end=-1, /)      : Returns the index of the first occurrence of a value.

# 🔧 Collection-Like Operators:
#     - __add__(self, value, /)                     : Concatenates two tuples (`+`).
#     - __mul__(self, value, /)                     : Repeats the tuple (`*`).
#     - __rmul__(self, value, /)                    : Reflects multiplication (`*`).

When the text related datatypes were examined, the immutable Sequence bytes was seen to have a MutableSequence counterpart bytearray. The MutableSequence has consistent immutable methods to its immutable Sequence as it follows the same design pattern. Each of these methods return a value. The MutableSequence however includes additional mutable methods, which typically don't return a value but instead modify the instance in place:

In [6]: dir(bytearray)
Out[6]: ['__add__', '__alloc__', '__class__', '__contains__', '__delattr__', 
         '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', 
         '__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__', 
         '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', 
         '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', 
         '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', 
         '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', 
         '__subclasshook__', 'append', 'capitalize', 'center', 'clear', 
         'copy', 'count', 'decode', 'endswith', 'expandtabs', 'extend', 
         'find', 'fromhex', 'hex', 'index', 'insert', 'isalnum', 'isalpha', 
         'isascii', 'isdigit', 'islower', 'isspace', 'istitle', 'isupper', 
         'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 
         'pop', 'remove', 'removeprefix', 'removesuffix', 'replace', 
         'reverse', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 
         'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 
         'title', 'translate', 'upper', 'zfill']

The tuple is an immutable Sequence that has a MutableSequence counterpart list:

In [7]: dir(list)
Out[7]: ['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__', 
         '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', 
         '__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__', 
         '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', 
         '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', 
         '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', 
         '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 
         'append', 'clear', 'copy', 'count', 'extend', 
         'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

The identifiers for the list class can also be examined:

In [8]: list.
# -------------------------------
# Available Identifiers for `list`:
# ---------------------------------

# 🔧 Functions from `object` (inherited by `list`):
#     - __init__(self, /, *args, **kwargs)             : Initializes the list object.
#     - __new__(*args, **kwargs)                       : Creates a new instance of the class.
#     - __delattr__(self, name, /)                     : Defines behavior for when an attribute is deleted.
#     - __dir__(self, /)                               : Default dir() implementation.
#     - __sizeof__(self, /)                            : Returns the size of the object in memory, in bytes.
#     - __eq__(self, value, /)                         : Checks for equality with another object.
#     - __ne__(self, value, /)                         : Checks for inequality with another object.
#     - __lt__(self, value, /)                         : Checks if the list is less than another.
#     - __le__(self, value, /)                         : Checks if the list is less than or equal to another.
#     - __gt__(self, value, /)                         : Checks if the list is greater than another.
#     - __ge__(self, value, /)                         : Checks if the list is greater than or equal to another.
#     - __repr__(self, /)                              : Returns a string representation of the list.
#     - __str__(self, /)                               : Returns a string for display purposes.
#     - __format__(self, format_spec, /)               : Returns a formatted string representation of the object.
#     - __hash__(self, /)                              : Returns a hash of the object.
#     - __getattribute__(self, name, /)                : Gets an attribute from the object.
#     - __setattr__(self, name, value, /)              : Sets an attribute on the object.
#     - __delattr__(self, name, /)                     : Deletes an attribute from the object.
#     - __reduce__(self, /)                            : Prepares the object for pickling.
#     - __reduce_ex__(self, protocol, /)               : Similar to __reduce__, with a protocol argument.

# 🔍 Attributes from `object`:
#     - __class__                                      : The class of the list object.
#     - __doc__                                        : The docstring of the list class.

# 🔧 Collection-Based Methods (from `list` and the Collection ABC):
#     - __contains__(self, item, /)                    : Checks if an item is in the list (`in`).
#     - __iter__(self, /)                              : Returns an iterator over the list.
#     - __len__(self, /)                               : Returns the length of the list.
#     - __getitem__(self, index, /)                    : Retrieves an item by index (`[]`).
#     - count(self, value, /)                          : Counts the occurrences of a value in the list.
#     - index(self, value, start=0, stop=None, /)      : Returns the index of the first occurrence of a value.

# 🔧 Mutable Collection-Specific Methods:
#     - __setitem__(self, index, value, /)             : Assigns a value to an item (`[] =`).
#     - __delitem__(self, index, /)                    : Deletes an item from the list.
#     - append(self, item, /)                          : Appends an item to the end of the list.
#     - extend(self, iterable, /)                      : Appends all items from an iterable to the list.
#     - insert(self, index, item, /)                   : Inserts an item at a specific position.
#     - pop(self, index=-1, /)                         : Removes and returns an item at a given index.
#     - remove(self, value, /)                         : Removes the first occurrence of a value.
#     - clear(self, /)                                 : Removes all items from the list.
#     - reverse(self, /)                               : Reverses the order of the list in place.

# 🔧 Collection-Like Operators:
#     - __add__(self, other, /)                        : Implements list concatenation (`+`).
#     - __mul__(self, other, /)                        : Implements list repetition (`*`).
#     - __rmul__(self, other, /)                       : Implements reflected multiplication (`*`).

# 🔧 Sorting and Copying:
#     - sort(self, key=None, reverse=False, /)         : Sorts the list in place.
#     - copy(self, /)                                  : Returns a shallow copy of the list.

A frozenset is a Set of unique elements, where each element is a reference to a Python object. A frozenset follows the design pattern of a Collection and therefore has collection based properties consistent to the tuple, str and bytes explored earlier. In a frozenset, the element is a unique object. The frozenset is not a Sequence and its elements are unordered. The set therefore lacks the data model method __getitem__ which is used to access an element using a numeric index in the other classes:

In [9]: dir(frozenset)
Out[9]: ['__and__', '__class__', '__class_getitem__', '__contains__', '__delattr__', 
         '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', 
         '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', 
         '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', 
         '__rand__', '__reduce__', '__reduce_ex__', '__repr__', '__ror__', '__rsub__', 
         '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', 
         '__xor__', 'copy', 'difference', 'intersection', 'isdisjoint', 'issubset', 
         'issuperset', 'symmetric_difference', 'union']

Details about its identifiers are shown:

In [10]: frozenset.
# -------------------------------
# Available Identifiers for `frozenset`:
# -------------------------------------

# 🔧 Functions from `object` (inherited by `frozenset`):
#     - __init__(self, /, *args, **kwargs)             : Initializes the frozenset.
#     - __new__(*args, **kwargs)                       : Creates a new instance of the frozenset class.
#     - __delattr__(self, name, /)                     : Deletes an attribute from the frozenset.
#     - __dir__(self, /)                               : Default dir() implementation.
#     - __repr__(self, /)                              : Returns a string representation of the frozenset.
#     - __str__(self, /)                               : Returns a string for display purposes.
#     - __hash__(self, /)                              : Returns a hash value for the frozenset (frozensets are hashable).
#     - __getattribute__(self, name, /)                : Gets an attribute from the frozenset.
#     - __setattr__(self, name, value, /)              : Sets an attribute on the frozenset (not applicable for immutable types).
#     - __delattr__(self, name, /)                     : Deletes an attribute from the frozenset (not applicable for immutable types).
#     - __reduce__(self, /)                            : Prepares the object for pickling.
#     - __reduce_ex__(self, protocol, /)               : Similar to __reduce__, with a protocol argument.
#     - __sizeof__(self, /)                            : Returns the size of the frozenset in memory, in bytes.
#     - __eq__(self, other, /)                         : Checks equality between frozensets.
#     - __ne__(self, other, /)                         : Checks inequality between frozensets.
#     - __lt__(self, other, /)                         : Checks if the frozenset is a strict subset of another.
#     - __le__(self, other, /)                         : Checks if the frozenset is a subset of another.
#     - __gt__(self, other, /)                         : Checks if the frozenset is a strict superset of another.
#     - __ge__(self, other, /)                         : Checks if the frozenset is a superset of another.

# 🔍 Attributes from `object`:
#     - __class__                                      : The class of the frozenset object.
#     - __doc__                                        : The docstring of the frozenset class.

# 🔧 Collection-Based Methods (from `frozenset`):
#     - __len__(self, /)                               : Returns the number of items in the frozenset.
#     - __contains__(self, item, /)                    : Checks if an item is in the frozenset (`in` operator).
#     - __iter__(self, /)                              : Returns an iterator over the frozenset.

# 🔄 Binary Frozenset Operators:
#     - __sub__(self, other, /)                        : Returns the difference between two frozensets (`-`).
#     - __and__(self, other, /)                        : Returns the intersection of two frozensets (`&`).
#     - __or__(self, other, /)                         : Returns the union of two frozensets (`|`).
#     - __xor__(self, other, /)                        : Returns the symmetric difference of two frozensets (`^`).

# 🔄 Immutable Frozenset Methods:
#     - intersection(self, *others)                     : Returns the intersection of this frozenset with others.
#     - union(self, *others)                            : Returns the union of this frozenset with others.
#     - difference(self, *others)                       : Returns the difference between this frozenset and others.
#     - symmetric_difference(self, other)               : Returns the symmetric difference between this frozenset and another.

# 🔧 Immutable Collection-Specific Methods:
#     - copy(self, /)                                  : Returns a shallow copy of the frozenset (returns the same object).

# 🔄 Frozenset Comparison Methods:
#     - isdisjoint(self, other, /)                     : Checks if two frozensets have no elements in common.
#     - issubset(self, other, /)                       : Checks if the frozenset is a subset of another.
#     - issuperset(self, other, /)                     : Checks if the frozenset is a superset of another.

The frozenset has an mutable counterpart set. Unlike the bytes/bytearray and tuple/list, where the immutable Sequence is the default, for frozenset/set, the MutableSet counterpart is the default. Note do not confuse the abstract base class Set (PascalCase) which is immutable, with the mutable class set (lowercase). For clarity set follows the design pattern of a MutableSet and Set, and frozenset follows the design pattern of a Set. The directory of the set can be examined:

In [10]: dir(set)
Out[10]: ['__and__', '__class__', '__class_getitem__', '__contains__', '__delattr__', 
         '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', 
         '__getstate__', '__gt__', '__hash__', '__iand__', '__init__', '__init_subclass__', 
         '__ior__', '__isub__', '__iter__', '__ixor__', '__le__', '__len__', '__lt__', 
         '__ne__', '__new__', '__or__', '__rand__', '__reduce__', '__reduce_ex__', 
         '__repr__', '__ror__', '__rsub__', '__rxor__', '__setattr__', '__sizeof__', 
         '__str__', '__sub__', '__subclasshook__', '__xor__', 'add', 'clear', 'copy', 
         'difference', 'difference_update', 'discard', 'intersection', 'intersection_update', 
         'isdisjoint', 'issubset', 'issuperset', 'pop', 'remove', 
         'symmetric_difference', 'symmetric_difference_update', 'union', 'update']

Details about its identifiers are shown:

In [11]: set.
# -------------------------------
# Available Identifiers for `set`:
# -------------------------------------

# 🔧 Functions from `object` (inherited by `set`):
#     - __init__(self, /, *args, **kwargs)             : Initializes the set.
#     - __new__(*args, **kwargs)                       : Creates a new instance of the set class.
#     - __delattr__(self, name, /)                     : Deletes an attribute from the set.
#     - __dir__(self, /)                               : Default dir() implementation.
#     - __repr__(self, /)                              : Returns a string representation of the set.
#     - __str__(self, /)                               : Returns a string for display purposes.
#     - __hash__(self, /)                              : Not implemented (sets are unhashable).
#     - __getattribute__(self, name, /)                : Gets an attribute from the set.
#     - __setattr__(self, name, value, /)              : Sets an attribute on the set.
#     - __delattr__(self, name, /)                     : Deletes an attribute from the set.
#     - __reduce__(self, /)                            : Prepares the object for pickling.
#     - __reduce_ex__(self, protocol, /)               : Similar to __reduce__, with a protocol argument.
#     - __sizeof__(self, /)                            : Returns the size of the set in memory, in bytes.
#     - __eq__(self, other, /)                         : Checks equality between sets.
#     - __ne__(self, other, /)                         : Checks inequality between sets.
#     - __lt__(self, other, /)                         : Checks if the set is a strict subset of another.
#     - __le__(self, other, /)                         : Checks if the set is a subset of another.
#     - __gt__(self, other, /)                         : Checks if the set is a strict superset of another.
#     - __ge__(self, other, /)                         : Checks if the set is a superset of another.

# 🔍 Attributes from `object`:
#     - __class__                                      : The class of the set object.
#     - __doc__                                        : The docstring of the set class.

# 🔧 Collection-Based Methods (from `set`):
#     - __len__(self, /)                               : Returns the number of items in the set.
#     - __contains__(self, item, /)                    : Checks if an item is in the set (`in` operator).
#     - __iter__(self, /)                              : Returns an iterator over the set.

# 🔄 Binary Set Operators:
#     - __sub__(self, other, /)                        : Returns the difference between two sets (`-`).
#     - __and__(self, other, /)                        : Returns the intersection of two sets (`&`).
#     - __or__(self, other, /)                         : Returns the union of two sets (`|`).
#     - __xor__(self, other, /)                        : Returns the symmetric difference of two sets (`^`).

# 🔄 Immutable Set Methods:
#     - intersection(self, *others)                     : Returns the intersection of this set with others.
#     - union(self, *others)                            : Returns the union of this set with others.
#     - difference(self, *others)                       : Returns the difference between this set and others.
#     - symmetric_difference(self, other)               : Returns the symmetric difference between this set and another.

# 🔄 Set Comparison Methods:
#     - isdisjoint(self, other, /)                     : Checks if two sets have no elements in common.
#     - issubset(self, other, /)                       : Checks if the set is a subset of another.
#     - issuperset(self, other, /)                     : Checks if the set is a superset of another.

# 🧩 Conversion and Copying:
#     - copy(self, /)                                  : Returns a shallow copy of the set.

# 🔧 Mutable Collection-Specific Methods:
#     - add(self, element, /)                          : Adds an element to the set.
#     - remove(self, element, /)                       : Removes a specified element; raises KeyError if not found.
#     - discard(self, element, /)                      : Removes a specified element if it exists.
#     - pop(self, /)                                   : Removes and returns an arbitrary element; raises KeyError if empty.
#     - clear(self, /)                                 : Removes all elements from the set.
#     - update(self, *others)                          : Updates the set with the union of itself and others.

# 🔄 Mutable Set Methods:
#     - intersection_update(self, *others)             : Updates the set with the intersection of itself and others.
#     - difference_update(self, *others)               : Updates the set with the difference between itself and others.
#     - symmetric_difference_update(self, other, /)    : Updates the set with the symmetric difference of itself and another.

# 🔄 In-Place Set Operators:
#     - __ior__(self, other, /)                        : Implements in-place union operation (`|=`).
#     - __iand__(self, other, /)                       : Implements in-place intersection operation (`&=`).
#     - __isub__(self, other, /)                       : Implements in-place difference operation (`-=`).
#     - __ixor__(self, other, /)                       : Implements in-place symmetric difference operation (`^=`).

A dict is a MutableMapping where each element is an item. An item has a key, which is usually an immutable instance usually in the form of a str instance that maps to a value which is an object. The ability to change items or add/remove keys is fundamental to the utility of a dict and therefore the dict has no builtins immutable counterpart. A Mapping is a child class of the abstract base class Collection and the dict therefore has collection like properties, where each element is an item. A Mapping is not a Sequence and although it is ordered by insertion order, it has no numeric index. In a Mapping the data model method __getitem__ is redefined to use the key to retrieve the value. The directory of a dict can be examined:

In [11]: dir(dict)
Out[11]: ['__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__', 
          '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', 
          '__getitem__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', 
          '__ior__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', 
          '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__ror__', '__setattr__', 
          '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 
          'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']

The identifiers of the dict are as follows:

In [12]: dict.
# -------------------------------
# Available Identifiers for `dict`:
# -------------------------------------

# 🔧 Functions from `object` (inherited by `dict`):
#     - __init__(self, /, *args, **kwargs)             : Initializes the dictionary.
#     - __new__(*args, **kwargs)                       : Creates a new instance of the dictionary class.
#     - __delattr__(self, name, /)                     : Deletes an attribute from the dictionary.
#     - __dir__(self, /)                               : Default dir() implementation.
#     - __repr__(self, /)                              : Returns a string representation of the dictionary.
#     - __str__(self, /)                               : Returns a string for display purposes.
#     - __hash__(self, /)                              : Not implemented (dictionaries are unhashable).
#     - __getattribute__(self, name, /)                : Gets an attribute from the dictionary.
#     - __setattr__(self, name, value, /)              : Sets an attribute on the dictionary.
#     - __delattr__(self, name, /)                     : Deletes an attribute from the dictionary.
#     - __reduce__(self, /)                            : Prepares the object for pickling.
#     - __reduce_ex__(self, protocol, /)               : Similar to __reduce__, with a protocol argument.
#     - __sizeof__(self, /)                            : Returns the size of the dictionary in memory, in bytes.
#     - __eq__(self, other, /)                         : Checks equality between dictionaries.
#     - __ne__(self, other, /)                         : Checks inequality between dictionaries.
#     - __lt__(self, other, /)                         : Checks if the dictionary is a strict subset of another.
#     - __le__(self, other, /)                         : Checks if the dictionary is a subset of another.
#     - __gt__(self, other, /)                         : Checks if the dictionary is a strict superset of another.
#     - __ge__(self, other, /)                         : Checks if the dictionary is a superset of another.

# 🔍 Attributes from `object`:
#     - __class__                                      : The class of the dictionary object.
#     - __doc__                                        : The docstring of the dictionary class.

# 🔧 Collection-Based Methods (from `dict`):
#     - __len__(self, /)                               : Returns the number of items in the dictionary.
#     - __contains__(self, key, /)                     : Checks if a key is in the dictionary (`in` operator).
#     - __iter__(self, /)                              : Returns an iterator over the dictionary's keys.
#     - __getitem__(self, key, /)                      : Returns the value associated with the key.

# 🧩 Conversion and Copying:
#     - copy(self, /)                                  : Returns a shallow copy of the dict.

# 🔄 Mapping-Specific Methods:
#     - keys(self, /)                                  : Returns a new view of the dictionary's keys.
#     - values(self, /)                                : Returns a new view of the dictionary's values.
#     - items(self, /)                                 : Returns a new view of the dictionary's items.
#     - get(self, key, default=None, /)                : Returns the value for the specified key, or default if not found.
#     - pop(self, key, /)                              : Removes the specified key and returns its value; raises KeyError if key not found.
#     - popitem(self, /)                               : Removes and returns the last (key, value) pair; raises KeyError if empty.
#     - clear(self, /)                                 : Removes all items from the dictionary.
#     - setdefault(self, key, default=None, /)         : Returns the value if key is in dictionary; inserts key with default value if not.
#     - update(self, /, *args, **kwargs)               : Updates the dictionary with another dictionary or iterable of key-value pairs.

# 🔄 Binary Dictionary Operators:
#     - __or__(self, other, /)                         : Merges two dictionaries (`|`).

# 🔧 Mutable Mapping-Specific Methods:
#     - __setitem__(self, key, value, /)               : Sets the value for the specified key.
#     - __delitem__(self, key, /)                      : Deletes the specified key from the dictionary.

# 🔄 In-Place Dictionary Operators:
#     - __ior__(self, other, /)                        : Implements in-place dictionary merging (`|=`).

tuple and list Collections

Supposing the 5 object instances are instantiated to the instance names a, b, c, d and e:

In [13]: a = object()
       : b = object()
       : c = object()
       : d = object()
       : e = object()

These five object instances can be grouped together into a Sequence by use of the , which acts as a delimiter:

In [14]: a, b, c, d, e
Out[14]: 
(<object at 0x20fdbe56af0>,
 <object at 0x20fdbe56b20>,
 <object at 0x20fdbe56aa0>,
 <object at 0x20fdbe56a00>,
 <object at 0x20fdbe56a10>)

Notice that Out[14] displays the formal representation of each object. A comma acts as a delimiter and the Sequence is enclosed in brackets (). The brackets indicate that this Sequence is Python's default Sequence which is the tuple and should not be confused with the same brackets () when used with parenthesis. The difference between a tuple and parenthesis is clear for a multiple element tuple but care needs to be taken to avoid confusion between parenthesis and a single element tuple as shown below:

In [15]: a,
Out[15]: (<object at 0x20fdbe56af0>,)

In [16]: a
Out[16]: <object at 0x20fdbe56af0>

In [17]: (a,)
Out[17]: (<object at 0x20fdbe56af0>,)

In [18]: (a)
Out[18]: <object at 0x20fdbe56af0>

Essentially the , is required after the single element, in order to instantiate a single element tuple as shown in In [15] and In [17]. Without this , any () is taken as parenthesis as shown in In [18].

An empty pair of () are used to instantiate an empty tuple:

In [19]: ()
Out[19]: ()

The tuple class can be used to explictly instantiate a tuple instance as shown in In [20] however Out[20] which is a print out of the formal representation shows that the shorthand form with the parenthesis is preferred:

In [20]: tuple((a, b, c, d, e))
Out[20]:
(<object at 0x20fdbe56af0>,
 <object at 0x20fdbe56b20>,
 <object at 0x20fdbe56aa0>,
 <object at 0x20fdbe56a00>,
 <object at 0x20fdbe56a10>)

A tuple can instantiated using the following syntax In [21], however notice that the print out of the formal representation Out[21] prefers inclusion of the parenthesis:

In [21]: a, b, c, d, e
Out[21]: 
(<object at 0x20fdbe56af0>,
 <object at 0x20fdbe56b20>,
 <object at 0x20fdbe56aa0>,
 <object at 0x20fdbe56a00>,
 <object at 0x20fdbe56a10>)

The () are normally used when a tuple is used directly for example tuple instantiation to an object name:

In [22]: nums = (1, 2, 3, 4, 5)

The () are normally omitted when a tuple is indirectly used for example when returning multiple instances from a function call. A function that returns multiple instances, normally does so indirectly via a tuple and therefore this is known as tuple unpacking. For example the function divmod has a return statement which returns multiple values via a tuple:

In [23]: divmod(5, 3)
Out[23]: (1, 2)

In [24]: quotient_remainder_pair = divmod(5, 3)

In [25]: (quotient2, remainder2) = divmod(5, 3)

In [26]: quotient1, remainder1 = divmod(5, 3)

Notice the tuple in Out[23] contains two int instances 1 and 2. Recall that an int is a child class of the object class and therefore can be used as an element in a tuple.

In In [24], the tuple returned is assigned to an object name quotient_remainder_pair.

In In [25] each component of the tuple returned is assigned to a separate object name. The tuple itself is not assigned to an object name and therefore the () are typically not included as shown in In [26] which is a more common syntax.

To delete multiple object names, tuple unpacking is normally used with the del statement. The parenthesis shown in In [27] are typically not included, like in In [28] which is more typical:

In [26]: del quotient_remainder_pair
In [27]: del (quotient2, remainder2) 
In [28]: del quotient1, remainder1

tuple unpacking is also used to swap instance names. For example:

In [29]: num1 = 1
       : num2 = 2
In [30]: (num1, num2) = (num2, num1)
       : # right hand side expression examined first
       : # (num1, num2) = (2, 1)
       : # assignment of each int instance made to respective object name to the left of = operator
In [31]: num1
       : 2
In [32]: num2
       : 1
In [33]: num1, num2
       : (2, 1) 
In [34]: num1, num2 = num2, num1
In [35]: num1, num2
Out[35]: (1, 2)

Once again the form shown in In [34] without the () is more typical than In [30].

tuple unpacking is typically used within the return statement of a function and In [37] is more typical than In [36]:

In [36]: def fun_1():
       :     return (1, 2)
       :
In [37]: def fun_2():
       :     return 1, 2

The immutable tuple should be preferentially used to group data together, that is not going to be later modified. The example of using a tuple to return multiple instances to object names is a good example because in this sue case the tuple created, itself was not assigned to an object name and is immediately cleaned up by Python's garbage collection. The additional overhead of a mutable instance is not required in this use case. Because many of the use cases highlighted above where a tuple is utilised is normally done so indirectly, begineer programmer often underutilise the tuple and overuse in favour for its mutable counterpart the list. The list is enclosed in square brackets []:

In [38]: [a, b, c, d, e]
Out[38]: 
[<object at 0x20fdbe56af0>,
 <object at 0x20fdbe56b20>,
 <object at 0x20fdbe56aa0>,
 <object at 0x20fdbe56a00>,
 <object at 0x20fdbe56a10>]

There is no ambiguity in the use of square brackets and a single element list can be instantiated without explict use of a , as shown in the return value Out[38]:

In [38]: [a,]
Out[38]: [<object at 0x20fdbe56af0>]

An empty pair of [] are used to instantiate an empty list:

In [39]: []
Out[39]: []

The list class can be used to explictly instantiate a list instance as shown in In [40] however Out[40] which is a print out of the formal representation shows that the shorthand form with the parenthesis is preferred:

In [40]: list([a, b, c, d, e])
Out[40]: 
[<object at 0x20fdbe56af0>,
 <object at 0x20fdbe56b20>,
 <object at 0x20fdbe56aa0>,
 <object at 0x20fdbe56a00>,
 <object at 0x20fdbe56a10>]

Typically the builtins classes are used to cast a class from one builtins to another:

In [41]: list((a, b, c, d, e))
Out[41]: 
[<object at 0x20fdbe56af0>,
 <object at 0x20fdbe56b20>,
 <object at 0x20fdbe56aa0>,
 <object at 0x20fdbe56a00>,
 <object at 0x20fdbe56a10>]

In [42]: tuple([a, b, c, d, e])
Out[42]: 
(<object at 0x20fdbe56af0>,
 <object at 0x20fdbe56b20>,
 <object at 0x20fdbe56aa0>,
 <object at 0x20fdbe56a00>,
 <object at 0x20fdbe56a10>)

Recall that the str and bytes classes are also Sequence and therefore can be cast into a tuple. The tuple instance casted from a str has a 1 Unicode character str as each element and the tuple cast from a bytes instance has a int between 0:255 as each element. Notice that Out[44] shows each element on a single line and Out[46] displays each element on an individual line. This is done because Out[46] is a longer tuple that cannot be fitted on one line:

In [43]: 'Γεια σου Κοσμο!'
       : 'Γεια σου Κοσμο!'

In [44]: tuple('Γεια σου Κοσμο!')
Out[44]: ('Γ', 'ε', 'ι', 'α', ' ', 'σ', 'ο', 'υ', ' ', 'Κ', 'ο', 'σ', 'μ', 'ο', '!')

In [45]: bytes('Γεια σου Κοσμο!', encoding='utf-8')
Out[45]: b'\xce\x93\xce\xb5\xce\xb9\xce\xb1 \xcf\x83\xce\xbf\xcf\x85 \xce\x9a\xce\xbf\xcf\x83\xce\xbc\xce\xbf!'

In [46]: tuple(bytes('Γεια σου Κοσμο!', encoding='utf-8'))
Out[46]: 
(206,
 147,
 206,
 181,
 206,
 185,
 206,
 177,
 32,
 207,
 131,
 206,
 191,
 207,
 133,
 32,
 206,
 154,
 206,
 191,
 207,
 131,
 206,
 188,
 206,
 191,
 33)

The tuple can also be written using multiple lines, showing multiple elements on each line as convenient:

In [47]: (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 
          131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 
          207, 131, 206, 188, 206, 191, 33)
Out[47]:
(206,
 147,
 206,
 181,
 206,
 185,
 206,
 177,
 32,
 207,
 131,
 206,
 191,
 207,
 133,
 32,
 206,
 154,
 206,
 191,
 207,
 131,
 206,
 188,
 206,
 191,
 33)

A bytes instance can be instantiated by casting a tuple of int instances:

In [48]: bytes((206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 
              131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 
              207, 131, 206, 188, 206, 191, 33))
Out[48]: b'\xce\x93\xce\xb5\xce\xb9\xce\xb1 \xcf\x83\xce\xbf\xcf\x85 \xce\x9a\xce\xbf\xcf\x83\xce\xbc\xce\xbf!'

If the following three instances are instaniated to object names:

In [44]: text = 'Γεια σου Κοσμο!'
       : text_b = bytes('Γεια σου Κοσμο!', encoding='utf-8')
       : archive = ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)

They can be viewed in the Variable Explorer:

Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 'Γεια σου Κοσμο!'
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)

The tuple instance archive can be explored in the Variable Explorer:

archive - tuple (8 elements)
Index ▲ Type Size Value
0 str 4 Γεια
1 str 4 Γεια
2 str 4 Γεια
3 int 1 1
4 int 1 1
5 bool 1 True
6 float 1 3.14
7 int 1 2

Notice that the tuple is an ordered Sequence that can contain duplicate references to the same object. Since the str and bytes are also immutable Collections they can also be conceptualised like a tuple:

text - str (15 elements)
Index ▲ Type Size Value
0 str 1 Γ
1 str 1 ε
2 str 1 ι
3 str 1 α
4 str 1
5 str 1 σ
6 str 1 ο
7 str 1 υ
8 str 1
9 str 1 Κ
10 str 1 ο
11 str 1 σ
12 str 1 μ
13 str 1 ο
14 str 1 !
text_b - bytes (27 elements)
Index ▲ Type Size Value
0 int 1 206
1 int 1 147
2 int 1 206
3 int 1 181
4 int 1 206
5 int 1 185
6 int 1 206
7 int 1 177
8 int 1 32
9 int 1 207
10 int 1 131
11 int 1 206
12 int 1 191
13 int 1 207
14 int 1 133
15 int 1 32
16 int 1 206
17 int 1 154
18 int 1 206
19 int 1 191
20 int 1 207
21 int 1 131
22 int 1 206
23 int 1 188
24 int 1 206
25 int 1 191
26 int 1 33

These are ordered Sequence and have a numeric index. The following Sequence based identifiers are therefore consistent:

# 🔧 Collection-Based Methods (Collection ABC):
#     - __contains__(self, key, /)                  : Checks if an element is in the Collection (`in`).
#     - __iter__(self, /)                           : Returns an iterator over the Collection.
#     - __len__(self, /)                            : Returns the number of elements in the Collection.
#     - __getitem__(self, key, /)                   : Retrieves an element by index (`[]`).
#     - count(self, value, /)                       : Counts the number of occurrences of a value.
#     - index(self, value, start=0, end=-1, /)      : Returns the index of the first occurrence of a value.

# 🔧 Collection-Like Operators:
#     - __add__(self, value, /)                     : Concatenates two Collections (`+`).
#     - __mul__(self, value, /)                     : Repeats the Collection (`*`).

The data model method __contains__ defines the behaviour of the in keyword and can be used to check if an element is in a Sequence:

In [45]: 'Γ' in text
Out[45]: True
In [46]: 'Ω' in text
Out[46]: False
In [47]: 'Γεια' in text
Out[47]: True
In [48]: 206 in text_b
Out[48]: True
In [49]: 255 in text_b
Out[49]: False
In [50]: bytes((206, 147, 206, 181)) in text_b
Out[50]: True
In [51]: 'Γεια' in archive
Out[51]: True
In [52]: 'Γ' in archive
Out[52]: False

Notice that Out[45] and Out[47] are True as a single element or substring are recognised as being in the string. Out[48] and Out[50] are likewise True as a single element or a subbyte are recognised being in the bytes object. Out[51] is True because the object, in this case the str instance 'Γεια' is in the tuple. Notice however that Out[52] is False, because an element in a tuple is a reference to a complete object. 'Γ' is the first letter in 'Γεια' but this it is not one of the complete str instances referenced by a tuple element.

The data model method __len__ defines the behaviour of the builtins function len and returns the integer number of elements:

In [53]: len(text)
Out[53]: 15
In [54]: len(text_b)
Out[54]: 27
In [55]: len(archive)
Out[55]: 8

The method count, counts the number of times an element occurs as shown in In [56]. In [58] and In [60]. For the str and bytes classes, this method will also count a substring and subbyte as shown in In [57] and In [58]:

In [56]: text.count('ο')
Out[56]: 3
In [57]: text.count('Γεια')
Out[57]: 1
In [58]: text_b.count(206)
Out[58]: 9
In [59]: text_b.count(bytes((206, 191)))
Out[59]: 3
In [60]: archive.count('Γεια')
Out[60]: 3
In [61]: archive.count(1)
Out[61]: 3
In [62]: True == 1
Out[62]: True
In [63]: archive.count(('Γεια', 'Γεια')) 
Out[63]: 0

Because the bool class is a child class of the int class with the instance True being equivalent to 1 when the number of counts for 1 as shown in In [61] returns 3 instead of 2. For a tuple, the method count only counts the number of elements and does not count a subtuple. Recall the element in a tuple is an object and the tuple itself is an object, so a tuple can be one of the elements in another tuple:

In [64]: archive2 = ('Γεια', 'Γεια', ('Γεια', 1, 1), True, 3.14, 2)
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 'Γεια σου Κοσμο!'
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
archive2 tuple 6 ('Γεια', 'Γεια', ('Γεια', 1, 1), True, 3.14, 2)
archive2 - tuple (6 elements)
Index ▲ Type Size Value
0 str 4 Γεια
1 str 4 Γεια
2 tuple 3 ('Γεια', 1, 1)
3 bool 1 True
4 float 1 3.14
5 int 1 2

The count method therefore only counts when an element is a tuple and does not consider a subtuple whic would result in confusion.

The data model method __getitem__ can be used to retrieve a value from a Sequence using the integer index. Recall that Python uses zero-order indexing and is inclusive of the start bound 0 and exclusive of the stop bound which is the length of the Sequence. Notice when each Sequence is expanded, the last element has the index that is the length of the Sequence minus one.

In [64]: text[0]
Out[64]: 'Γ'
In [65]: text[1]
Out[65]: 'ε'
In [66]: text[len(text)]
Traceback (most recent call last):

  Cell In[66], line 1
    text[len(text)]

IndexError: string index out of range
In [67]: text[len(text)-1]
Out[67]: '!'
In [68]: archive[0]
Out[68]: 'Γεια'
In [69]: archive[1]
Out[69]: 'Γεια'
In [70]: archive[len(archive)]
Traceback (most recent call last):

  Cell In[365], line 1
    archive[len(archive)]

IndexError: tuple index out of range
In [71]: archive[len(archive)-1]
Out[71]: 2

Indexing can also be performed with a slice instance. Recall slicing is inclusive of the start bound 0, and gos up in the specified steps to but not including the stop bound 4, in this case because the step is 1, the last value indexed is at 3:

In [72]: text[slice(0, 4, 1)]
Out[72]: 'Γεια'
In [73]: archive[slice(0, 4, 1)]
Out[73]: ('Γεια', 'Γεια', 'Γεια', 1)

For short the colon : is normally used:

In [74]: text[0:4:1]
Out[74]: 'Γεια'
In [75]: text[0:4:] # default step 1
Out[75]: 'Γεια'
In [76]: text[0:4] # default step 1
Out[76]: 'Γεια'
In [77]: text[:4] # default start 0
Out[77]: 'Γεια'
In [78]: text[9:len(text)]
Out[78]: 'Κοσμο!'
In [79]: text[9:] # default stop len(text)
Out[79]: 'Κοσμο!'
In [80]: archive[2:5]
Out[80]: ('Γεια', 1, 1)

The method index can be used to retrieve the numeric index of the first occurance of a value in a Sequence:

In [81]: text.index('ο')
Out[81]: 6

The optional start and end input parameters can be used to restrict the search range, since 'ο' was found at index 6, the next value can be searched after this:

In [82]: text.index('ο', 7)
Out[82]: 10

This is normally used in combination with the count method:

In [83]: archive.count('Γεια')
Out[83]: 3
In [84]: archive.index('Γεια')
Out[84]: 0
In [85]: archive.index('Γεια', 1)
Out[85]: 1
In [86]: archive.index('Γεια', 2)
Out[86]: 2

For the str a substr can be indexed, which gives the index of the start of the substr:

In [87]: text.index('εια')
Out[87]: 1

The index method in tuple, will only search for a complete reference to an object.

The data model methods __add__ and __mul__ are defined which perform Sequence concatenation (of the same type of Sequence) and replication with an int respectively:

In [88]: ('Γεια', 'Γεια', 'Γεια') + (1, 2, 3)
Out[88]: ('Γεια', 'Γεια', 'Γεια', 1, 2, 3)

In [89]: 'Γεια' + 'σου'
Out[89]: 'Γειασου'

In [90]: ('Γεια', 1) * 5
Out[90]: ('Γεια', 1, 'Γεια', 1, 'Γεια', 1, 'Γεια', 1, 'Γεια', 1)

In [91]: 'Γεια' * 5
Out[91]: 'ΓειαΓειαΓειαΓειαΓεια'

__rmul__ is also defined allowing reverse multiplication:

In [91]: 5 * ('Γεια', 1)
Out[91]: ('Γεια', 1, 'Γεια', 1, 'Γεια', 1, 'Γεια', 1, 'Γεια', 1)

The data model method __iter__ means the iter function can be used on the Sequence to return an iterator. Recall an iterator only displays one value at a time. The next function can be used to consume the current value and move onto the next value:

In [92]: archive_iterator = iter(archive)
In [93]: type(archive_iterator)
Out[93]: tuple_iterator
In [94]: next(archive_iterator)
Out[94]: 'Γεια'
In [94]: next(archive_iterator)
Out[94]: 'Γεια'
In [95]: next(archive_iterator)
Out[95]: 'Γεια'
In [96]: next(archive_iterator)
Out[96]: 1

The remaining values can be consumed by casting archive_iterator to a tuple:

In [97]: tuple(archive_iterator)
Out[97]: (1, True, 3.14, 2)

A for loop uses an iterator from behind the scenes. The general syntax is:

for element in collection_instance:
    pass
    pass
    pass

The : indicates the beginning of a code block, and each line belonging to the code block is indented by 4 spaces. In the example below, each element in archive is being looped over and printed:

In [98]: for element in archive:
       :     print(element)
       :
Γεια
Γεια
Γεια
1
1
True
3.14
2

Behind the scenes a for loop is a while loop that utilises an iterator:

In [99]: archive_iterator = iter(archive)
       : while True:
       :     try:
       :         print(next(archive_iterator))
       :     except StopIteration:
       :         break
       :
Γεια
Γεια
Γεια
1
1
True
3.14
2

Behind the scenes an iterator is used and is advanced during each iteration of the for loop. This means element is updated to each value in tuple during each iteration of the for loop.

In the for loop, element is a loop variable and can be accessed within the for loops code block. This variable can be named anything else that satisfies the rules behind identifier names. For example:

In [100]: for loop_variable in archive:
        :     print(loop_variable)
        :
Γεια
Γεια
Γεια
1
1
True
3.14
2

If the index is required, it is common to enumerate the Sequence. The enumerate object is essentially a tuple where the 1st element is the original index and the 2nd element is the element from the enumerated Sequence:

In [101]: enumerated_archive = enumerate(archive)
In [101]: next(enumerated_archive)
Out[101]: (0, 'Γεια')
In [102]: next(enumerated_archive)
Out[102]: (1, 'Γεια')
In [103]: next(enumerated_archive)
Out[103]: (2, 'Γεια')

Explicitly:

In [104]: for (index, element) in enumerate(archive):
        :     print((index, element))
        :
(0, 'Γεια')
(1, 'Γεια')
(2, 'Γεια')
(3, 1)
(4, 1)
(5, True)
(6, 3.14)
(7, 2)

However tuple packing is typically used:

In [105]: for index, element in enumerate(archive):
        :     print((index, element))
        :
(0, 'Γεια')
(1, 'Γεια')
(2, 'Γεια')
(3, 1)
(4, 1)
(5, True)
(6, 3.14)
(7, 2)

And the index and element can be used within a formatted string:

In [106]: for index, element in enumerate(archive):
        :     print(f'{index}: {element}')
        :
0: Γεια
1: Γεια
2: Γεια
3: 1
4: 1
5: True
6: 3.14
7: 2

In the above use case, a for loop was used to read each element from the tuple instance archive and print it. Recall the tuple is read-only. A for loop can be used to create a new tuple:

In [107]: numbers = (1, 2, 3) 
        : doubled_numbers = ()
        : for number in numbers:
        :     doubled_numbers = doubled_numbers + (2*number,)
        :
        : doubled_numbers
Out[107]: (2, 4, 6)

Every object has an object identification and if the object is immutable it will also have a hash value that can be used to identify it. If the identification function id is used on the tuple instance doubled_numbers:

In[108]: numbers = (1, 2, 3) 
       : doubled_numbers = ()
       : print(f'{doubled_numbers} {id(doubled_numbers)}')
       : for number in numbers:
       :     doubled_numbers = doubled_numbers + (2*number,)
       :     print(f'{doubled_numbers} {id(doubled_numbers)}')
       :
() 140716795299288
(2,) 2680215366064
(2, 4) 2680216207872
(2, 4, 6) 2680093561984

Notice the id of doubled_numbers changes after each iterator of the for loop. This is because a new tuple instance is being created in each iteration of the for loops and is reassigned to doubled_numbers.

This type of operation is normally done with a list which has the mutable method append:

In[109]: numbers = (1, 2, 3) 
       : doubled_numbers = []
       : print(f'{doubled_numbers} {id(doubled_numbers)}')
       : for number in numbers:
       :     doubled_numbers.append(2*number)
       :     print(f'{doubled_numbers} {id(doubled_numbers)}')
    
[] 2680163025216
[2] 2680163025216
[2, 4] 2680163025216
[2, 4, 6] 2680163025216

Simplying this down and removing the print statements:

In [110]: numbers = (1, 2, 3) 
        : doubled_numbers = []
        : for number in numbers:
        :     doubled_numbers.append(2*number)
        :
        : doubled_numbers
Out[110]: [2, 4, 6]

Because this is done so commonly, there is a shorthand way known as a list comprehension:

In [111]: numbers = (1, 2, 3) 
        : doubled_numbers = [2*number for number in numbers]
        : doubled_numbers
Out[111]: [2, 4, 6]

For clarity, the list comprehension building up In [112] is:

In [112]: [] # list
In [112]: [2*number] # expression using loop variable
In [112]: [2*number for number in (1, 2, 3)] # for loop iteration updating loop variable
Out[112]: [2, 4, 6]

Notice that the tuple is being read and the list is being writed to. Because a tuple is read only and cannot be modified it has a hash value. Notice that the instantiated tuple instance (1, 2, 3) to nums is the same as the tuple instance (1, 2, 3) and therefore they have the same hash value and also the same identification as one another:

In [113]: nums = (1, 2, 3)
In [114]: hash(nums)
Out[114]: 529344067295497451
In [115]: hash((1, 2, 3))
Out[115]: 529344067295497451
In [116]: id(nums)
Out[116]: 2680064758464
In [117]: id((1, 2, 3))
Out[117]: 2680101762368

If the list is compared, notice because it is mutable it is not hashable. The data model identifier __hash__ is defined as None in the list class:

In [118]: doubled_numbers = [2, 4, 6]
In [119]: hash(doubled_numbers)
Traceback (most recent call last):

  Cell In[119], line 1
    hash(doubled_numbers)

TypeError: unhashable type: 'list'

If the identification is instead checked. Notice that doubled_numbers has a different identification to [2, 4, 6]:

In [120]: id(doubled_numbers)
Out[120]: 2680248957888
In [121]: id([2, 4, 6])
Out[121]: 2680215522368

Although they have equal values, they are different list instances. Therefore the is equal to operator == and is keyword yield differing results:

In [122]: doubled_numbers == [2, 4, 6]
Out[122]: True

In [122]: doubled_numbers is [2, 4, 6]
Out[122]: False

A hashable datatype therefore has some ambiguity and cannot be used as an element in a set or a key in a dict which rely on the hash values. Note that a tuple that references an immutable element such as a list is not hashable:

In [123]: hash((1, 2, 3, [4, 5, 6], 7))
Traceback (most recent call last):

  Cell In[444], line 1
    hash((1, 2, 3, [4, 5, 6], 7))

TypeError: unhashable type: 'list'

The comparison operator is equal == was used above. In a Sequence the six comparison operators are used and comparisons are normally made element by element using the respective element:

In [124]: (1, 2, 3, 5) > (1, 2, 4, 5)
        : # 1 == 1 # True → next element
        : # 2 == 2 # True → next element
        : # 3 == 4 # False → compare element
        : # 3 > 4 # False
Out[124]: False

Therefore the respective data types at each element must be compatible for the comparison operator to work:

In [125]: (1, 2, 3, 5) > (1, 2, '4', 5)
        : # 1 == 1 # True → next element
        : # 2 == 2 # True → next element
        : # 3 == '4' # Type Error
Traceback (most recent call last):

  Cell In[445], line 1
    (1, 2, 3, 5) > (1, 2, '4', 5)

TypeError: '>' not supported between instances of 'int' and 'str'

If a tuple is being compared to a tuple that has an additional element, the tuple with the additional element is considered greater:

In [126]: (1, 2, 5) > (1, 2, 5, 7)
        : # 1 == 1 # True → next element
        : # 2 == 2 # True → next element
        : # 5 == 5 # True → next element
        : # No element > element
Out[126]: False
In [127]: (1, 9, 5) > (1, 2, 5, 7)
        : # 1 == 1 # True → next element
        : # 9 == 2 # False
        : # 9 > 2 # True 
Out[127]: True

Because a byte is an ordinal int between 0:256, element by element comparisons compare the integers:

In [128]: bytes('Γεια', encoding='utf8')
Out[128]: b'\xce\x93\xce\xb5\xce\xb9\xce\xb1'

In [129]: bytes('Γειβ', encoding='utf8')
Out[129]: b'\xce\x93\xce\xb5\xce\xb9\xce\xb2'

In [130]: bytes('Γεια', encoding='utf8') > bytes('Γειβ', encoding='utf8')
Out[130]: False

The comparison is clearer when each bytes instance is examined as a tuple:

In [131]: tuple(b'\xce\x93\xce\xb5\xce\xb9\xce\xb1')
Out[131]: (206, 147, 206, 181, 206, 185, 206, 177)
In [132]: tuple(b'\xce\x93\xce\xb5\xce\xb9\xce\xb2')
Out[132]: (206, 147, 206, 181, 206, 185, 206, 178)

The str is a Sequence of Unicode characters. Each Unicode character has an ordinal value which can be calculated using the ordinal function ord and this ordinal value in the form of an int is used to compare Unicode character to Unicode character:

In [133]: ord('Γ')
Out[133]: 915

The int returned is the value of the 4 byte sequence using utf-32-be encoding. This is more clear when formatted correctly in hexadecimal:

In [134]: format(915, '#010x')
Out[134]: '0x00000393'

This can be seen to be equivalent to the bytes instance of the character encoded using utf-32-be:

In [135]: bytes('Γ', encoding='utf-32-be').hex()
Out[135]: '00000393'

Recall that the utf-32-be is used to insert a Unicode escape character:

In [136]: '\U00000393'
Out[136]: 'Γ'

The list and the bytearray follow the design pattern of a MutableCollection and therefore have the following mutable methods not available in their immutable counterparts:

# 🔧 Mutable Collection-Specific Methods:
#     - __setitem__(self, index, value, /)             : Assigns a value to an item (`[] =`).
#     - __delitem__(self, index, /)                    : Deletes an item from the list.
#     - append(self, item, /)                          : Appends an item to the end of the list.
#     - extend(self, iterable, /)                      : Appends all items from an iterable to the list.
#     - insert(self, index, item, /)                   : Inserts an item at a specific position.
#     - pop(self, index=-1, /)                         : Removes and returns an item at a given index.
#     - remove(self, value, /)                         : Removes the first occurrence of a value.
#     - clear(self, /)                                 : Removes all items from the list.
#     - reverse(self, /)                               : Reverses the order of the list in place.

# 🔧 Sorting and Copying:
#     - sort(self, key=None, reverse=False, /)         : Sorts the list in place.
#     - copy(self, /)                                  : Returns a shallow copy of the list.

Recall that a bytearray is normally instantiated by casting a bytes instance to a bytearray:

In [137]: text_b
Out[137]: b'\xce\x93\xce\xb5\xce\xb9\xce\xb1 \xcf\x83\xce\xbf\xcf\x85 \xce\x9a\xce\xbf\xcf\x83\xce\xbc\xce\xbf!'

In [138]: bytearray(text_b)
Out[138]: bytearray(b'\xce\x93\xce\xb5\xce\xb9\xce\xb1 \xcf\x83\xce\xbf\xcf\x85 \xce\x9a\xce\xbf\xcf\x83\xce\xbc\xce\xbf!')

However it can be helpful to conceptualise this as a tuple where each element is a byte i.e. an int between 0:256:

In [139]: tuple(text_b)
Out[139]: (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)

In [140]: bytearray((206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33))
Out[140]: bytearray(b'\xce\x93\xce\xb5\xce\xb9\xce\xb1 \xcf\x83\xce\xbf\xcf\x85 \xce\x9a\xce\xbf\xcf\x83\xce\xbc\xce\xbf!')

The mutable counterparts can be instantiated to instance names:

In [141] mutable_archive = list(archive)
In [142] mutable_text_b = bytearray(text_b)

These can be viewed in the Variable Explorer. Note the Variable Explorer in Spyder displays a bytes instance as text as it attempts to decode the bytes instance using utf-8 encoding and does not support a bytearray.

Some modifications have been made below to display the bytes instance as a tuple of byte elements and the bytearray instance as a list of byte elements where each byte element is an int instance between 0:256:

Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 27 [206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 8 ['Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2]

The append method can be used to append an element to the end of a MutableCollection. In the case of a list this can be any object for example 3:

In [141]: mutable_archive.append(3)

Notice that there is no Out[141] because this is a mutable method and modifies the instance in place. If the Variable Explorer is examined, notice that mutable_archive now includes the appended element 3:

Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 27 [206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 9 ['Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2, 3]

In the case of a bytearray this can be any byte element expressed as an int between 0:256 for example 3:

In [141]: mutable_text_b.append(3)
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 28 [206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 9 ['Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2, 3]

Recall the list is a MutableCollection of object elements. The object element can be another Sequence for example, the tuple instance (1, 2, 3):

In [141]: mutable_archive.append((1, 2, 3))
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 28 [206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 10 ['Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2, 3, (1, 2, 3)]

Notice that the length of mutable_archive is 10 because there are 10 elements in this list. This can be seen better when mutable_archive is explored:

mutable_archive - list (10 elements)
Index ▲ Type Size Value
0 str 4 Γεια
1 str 4 Γεια
2 str 4 Γεια
3 int 1 1
4 int 1 1
5 bool 1 True
6 float 1 3.14
7 int 1 2
8 int 1 3
9 tuple 3 (1, 2, 3)

The last element is a tuple with a size of 3 however in mutable_archive, this tuple is recognised as a single object and so the size of mutable_archive is 10 and not 12.

The extend method will extend a MutableCollection using the elements of another Sequence. For example:

In [142]: mutable_archive.extend((4, 5, 6))
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 28 [206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 13 ['Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2, 3, (1, 2, 3), 4, 5, 6]
mutable_archive - list (13 elements)
Index ▲ Type Size Value
0 str 4 Γεια
1 str 4 Γεια
2 str 4 Γεια
3 int 1 1
4 int 1 1
5 bool 1 True
6 float 1 3.14
7 int 1 2
8 int 1 3
9 tuple 3 (1, 2, 3)
10 int 1 4
11 int 1 5
12 int 1 6
In [143]: mutable_text_b.extend((4, 5, 6))
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 31 [206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3, 4, 5, 6]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 13 ['Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2, 3, (1, 2, 3), 4, 5, 6]

An immutable Sequence has the data model method __getitem__ which can be used to retrieve an item using its numeric index and square brackets:

In [144]: archive[0]
Out[144]: 'Γεια'

In [145]: archive[0:3]
Out[145]: ('Γεια', 'Γεια', 'Γεια')

The MutableCollection has the complementary mutable data model method __setitem__ which can be used to reassign an element or a slice of elements to a new element or slice of elements:

In [146]: id(mutable_archive)
Out[146]: 2764854194368

In [147]: mutable_archive[0] = 'hello'

Notice that mutable_archive is mutated in place:

Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 31 [206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3, 4, 5, 6]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 13 ['hello', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2, 3, (1, 2, 3), 4, 5, 6]

And retains the same identification:

In [148]: id(mutable_archive)
Out[148]: 2764854194368

This should not be confused with reassignment of the object name mutable_archive to a new list:

In [149]: mutable_archive = ['hello', 'hi', 'Γεια', 1, 1, True, 3.14, 2, 3, (1, 2, 3), 4, 5, 6]
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 31 [206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3, 4, 5, 6]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 13 ['hello', 'hi', 'Γεια', 1, 1, True, 3.14, 2, 3, (1, 2, 3), 4, 5, 6]

Notice this reassignment changes the identification:

In [150]: id(mutable_archive)
Out[150]: 2764857252224

A slice of a MutableCollection can also be assigned to a compatible object:

In [151]: mutable_archive[0:3] = 'x'

Notice the size which was 11 is now 13 because a slice of length 3 was assigned to a single object:

Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 31 [206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3, 4, 5, 6]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 11 ['x', 1, 1, True, 3.14, 2, 3, (1, 2, 3), 4, 5, 6]

The identification remains the same:

In [152]: id(mutable_archive)
Out[152]: 2764857252224

A slice can also be assigned to a compatible Sequence:

In [153]: mutable_archive[0:2] = ('a', 'b', 'c', 'd', 'e')

Notice the size which was 11 is now 13 because a slice of length 3 was assigned to a slice of length 5. The behaviour of using a slice is similar to use of the extend method:

Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 31 [206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3, 4, 5, 6]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 14 ['a', 'b', 'c', 'd', 'e', 1, True, 3.14, 2, 3, (1, 2, 3), 4, 5, 6]

A slice of a bytearray cannot be assigned to any object and the TypeError outlines what can essentially be cast into a bytearray when the slice is reassigned:

In [154]: mutable_text_b[0:3] = 255
Traceback (most recent call last):

  Cell In[154], line 1
    mutable_text_b[0:3] = 255

TypeError: can assign only bytes, buffers, or iterables of ints in range(0, 256)

The slice can be reassigned into a tuple of integers in the range 0:256:

In [155]: mutable_text_b[0:3] = (255,)
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 29 [255, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3, 4, 5, 6]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 14 ['a', 'b', 'c', 'd', 'e', 1, True, 3.14, 2, 3, (1, 2, 3), 4, 5, 6]

The data model __delitem__ can be used to delete an element or slice of elements in a MutableCollection:

In [256]: del mutable_archive[8]
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 29 [255, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3, 4, 5, 6]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 13 ['a', 'b', 'c', 'd', 'e', True, 3.14, 2, 3, (1, 2, 3), 4, 5, 6]
In [257]: del mutable_archive[0:5]
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 29 [255, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3, 4, 5, 6]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 8 [True, 3.14, 2, 3, (1, 2, 3), 4, 5, 6]

The mutable method insert can be used to insert an element at a specified index:

In [258]: mutable_archive.insert(3, 'hi')
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 29 [255, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3, 4, 5, 6]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 9 [True, 3.14, 2, 'hi', 3, (1, 2, 3), 4, 5, 6]

When used with a Sequence the behaviour of insert is more similar to that of append, inserting the Sequence at the specified index:

In [259]: mutable_archive.insert(3, ('a', 'b', 'c'))
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 29 [255, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3, 4, 5, 6]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 10 [True, 3.14, 2, ('a', 'b', 'c'), 'hi', 3, (1, 2, 3), 4, 5, 6]

The remove method will remove the first occurance of an element that has a specified value:

In [260]: mutable_text_b.remove(206)

Notice that only the element that was at index 2 that had the value 206 was removed, and this method behaves more similarly to append acting on a single element:

Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 28 [255, 181, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3, 4, 5, 6]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 10 [True, 3.14, 2, ('a', 'b', 'c'), 'hi', 3, (1, 2, 3), 4, 5, 6]

The immutable Sequence has the the data model methods __add__ and __mul__ which perform concatenation and replication with an int instance:

In [261]: mutable_archive + ['x', 'y', 'z']
Out[261]: [True, 3.14, 2, ('a', 'b', 'c'), 'hi', 3, (1, 2, 3), 4, 5, 6, 'x', 'y', 'z']

In [262]: mutable_archive * 2
Out[262]: [True,
           3.14,
           2,
           ('a', 'b', 'c'),
           'hi',
           3,
           (1, 2, 3),
           4,
           5,
           6,
           True,
           3.14,
           2,
           ('a', 'b', 'c'),
           'hi',
           3,
           (1, 2, 3),
           4,
           5,
           6]

The MutableCollection has the inplace counterparts __iadd__ and __imul__:

In [263]: mutable_archive += ['X', 'Y', 'Z']
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 28 [255, 181, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3, 4, 5, 6]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 13 [True, 3.14, 2, ('a', 'b', 'c'), 'hi', 3, (1, 2, 3), 4, 5, 6, 'X', 'Y', 'Z']
In [264]: mutable_archive *= 3
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 28 [255, 181, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3, 4, 5, 6]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 39 [True, 3.14, 2, ('a', 'b', 'c'), 'hi', 3, (1, 2, 3), 4, 5, 6, 'X', 'Y', 'Z', True, 3.14, 2, ('a', 'b', 'c'), 'hi', 3, (1, 2, 3), 4, 5, 6, 'X', 'Y', 'Z', True, 3.14, 2, ('a', 'b', 'c'), 'hi', 3, (1, 2, 3), 4, 5, 6, 'X', 'Y', 'Z']

The mutable method pop pops off an element from a MutableCollection and returns it. The pop method is unique, as most mutable methods mutate the MutableCollection inplace and return None:

In [265]: mutable_archive.pop()
Out[265]: 'Z'
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 28 [255, 181, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3, 4, 5, 6]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 38 [True, 3.14, 2, ('a', 'b', 'c'), 'hi', 3, (1, 2, 3), 4, 5, 6, 'X', 'Y', 'Z', True, 3.14, 2, ('a', 'b', 'c'), 'hi', 3, (1, 2, 3), 4, 5, 6, 'X', 'Y', 'Z', True, 3.14, 2, ('a', 'b', 'c'), 'hi', 3, (1, 2, 3), 4, 5, 6, 'X', 'Y']

The index of the element to be popped can be specified:

In [266]: mutable_archive.pop(3)
Out[266]: ('a', 'b', 'c')
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 28 [255, 181, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3, 4, 5, 6]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 37 [True, 3.14, 2, 'hi', 3, (1, 2, 3), 4, 5, 6, 'X', 'Y', 'Z', True, 3.14, 2, ('a', 'b', 'c'), 'hi', 3, (1, 2, 3), 4, 5, 6, 'X', 'Y', 'Z', True, 3.14, 2, ('a', 'b', 'c'), 'hi', 3, (1, 2, 3), 4, 5, 6, 'X', 'Y']
In [267]: popped_value = mutable_archive.pop(3)
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 28 [255, 181, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3, 4, 5, 6]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 36 [True, 3.14, 2, 3, (1, 2, 3), 4, 5, 6, 'X', 'Y', 'Z', True, 3.14, 2, ('a', 'b', 'c'), 'hi', 3, (1, 2, 3), 4, 5, 6, 'X', 'Y', 'Z', True, 3.14, 2, ('a', 'b', 'c'), 'hi', 3, (1, 2, 3), 4, 5, 6, 'X', 'Y']
popped_value str 2 hi

The mutable method reverse reverses the order of elements in a MutableCollection:

In [268]: mutable_archive.reverse()
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 28 [255, 181, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33, 3, 4, 5, 6]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 36 ['Y', 'X', 6, 5, 4, (1, 2, 3), 3, 'hi', ('a', 'b', 'c'), 2, 3.14, True, 'Z', 'Y', 'X', 6, 5, 4, (1, 2, 3), 3, 'hi', ('a', 'b', 'c'), 2, 3.14, True, 'Z', 'Y', 'X', 6, 5, 4, (1, 2, 3), 3, 2, 3.14, True]
popped_value str 2 hi

The mutable method sort sorts the order of elements in a MutableCollection. For this to work the elements have to be comparible with one another otherwise a TypeError is flagged up:

In [268]: mutable_archive.sort()
Traceback (most recent call last):

  Cell In[268], line 1
    mutable_archive.sort()

TypeError: '<' not supported between instances of 'tuple' and 'int'
In [269]: mutable_text_b.sort()
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 28 [3, 4, 5, 6, 32, 32, 33, 131, 131, 133, 154, 177, 181, 185, 188, 191, 191, 191, 206, 206, 206, 206, 206, 206, 207, 207, 207, 255]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 36 ['Y', 'X', 6, 5, 4, (1, 2, 3), 3, 'hi', ('a', 'b', 'c'), 2, 3.14, True, 'Z', 'Y', 'X', 6, 5, 4, (1, 2, 3), 3, 'hi', ('a', 'b', 'c'), 2, 3.14, True, 'Z', 'Y', 'X', 6, 5, 4, (1, 2, 3), 3, 2, 3.14, True]
popped_value str 2 hi

The method copy can be used to copy a MutableCollection:

In [270]: mutable_archive_copy = mutable_archive.copy()
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 28 [3, 4, 5, 6, 32, 32, 33, 131, 131, 133, 154, 177, 181, 185, 188, 191, 191, 191, 206, 206, 206, 206, 206, 206, 207, 207, 207, 255]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 36 ['Y', 'X', 6, 5, 4, (1, 2, 3), 3, 'hi', ('a', 'b', 'c'), 2, 3.14, True, 'Z', 'Y', 'X', 6, 5, 4, (1, 2, 3), 3, 'hi', ('a', 'b', 'c'), 2, 3.14, True, 'Z', 'Y', 'X', 6, 5, 4, (1, 2, 3), 3, 2, 3.14, True]
mutable_archive_copy list 36 ['Y', 'X', 6, 5, 4, (1, 2, 3), 3, 'hi', ('a', 'b', 'c'), 2, 3.14, True, 'Z', 'Y', 'X', 6, 5, 4, (1, 2, 3), 3, 'hi', ('a', 'b', 'c'), 2, 3.14, True, 'Z', 'Y', 'X', 6, 5, 4, (1, 2, 3), 3, 2, 3.14, True]
popped_value str 2 hi

Notice that the copy and the original have different identifications as they are each a unique object:

In [271]: id(mutable_archive)
Out[271]: 2764857252224
In [272]: id(mutable_archive_copy)
Out[272]: 2764856901952

They are equal to one another but are not the same object:

In [273]: mutable_archive == mutable_archive_copy
Out[273]: True

In [273]: mutable_archive is mutable_archive_copy
Out[273]: False

If a mutable method such as clear is used on the original, it will not effect the copy:

In [274]: mutable_archive.clear()
Variable Explorer
Name ▲ Type Size Value
text str 15 Γεια σου Κοσμο!
text_b bytes 27 (206, 147, 206, 181, 206, 185, 206, 177, 32, 207, 131, 206, 191, 207, 133, 32, 206, 154, 206, 191, 207, 131, 206, 188, 206, 191, 33)
mutable_text_b bytearray 28 [3, 4, 5, 6, 32, 32, 33, 131, 131, 133, 154, 177, 181, 185, 188, 191, 191, 191, 206, 206, 206, 206, 206, 206, 207, 207, 207, 255]
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
mutable_archive list 0 []
mutable_archive_copy list 36 ['Y', 'X', 6, 5, 4, (1, 2, 3), 3, 'hi', ('a', 'b', 'c'), 2, 3.14, True, 'Z', 'Y', 'X', 6, 5, 4, (1, 2, 3), 3, 'hi', ('a', 'b', 'c'), 2, 3.14, True, 'Z', 'Y', 'X', 6, 5, 4, (1, 2, 3), 3, 2, 3.14, True]
popped_value str 2 hi
In [275]: exit

set (and frozenset) Collections

A set is a Collection of unique object instances. The set is unordered and does not have the ordered properties seen in a Sequence. This can be seen clearly when a tuple is cast into a set:

In [1]: archive = ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
      : unique_archive = set(archive)

Notice the order of elements in the unique_archive is random and does not reflect the index from archive:

Variable Explorer
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
unique_archive set 4 {2, 1, 'Γεια', 3.14}

These can be explored using the Variable Explorer:

archive - tuple (8 elements)
Index ▲ Type Size Value
0 str 4 Γεια
1 str 4 Γεια
2 str 4 Γεια
3 int 1 1
4 int 1 1
5 bool 1 True
6 float 1 3.14
7 int 1 2

Note the set has no numeric index and all the duplicates in archive are no longer present:

mutable_archive - set (4 elements)
Type Size Value
int 1 2
int 1 1
str 4 Γεια
float 1 3.14

As a consequence the unique_archive does not have the Sequence behaviour and lacks the data model identifiers __getitem__, count and index:

In [2]: unique_archive[0]
Traceback (most recent call last):

  Cell In[2], line 1
    unique_archive[0]

TypeError: 'set' object is not subscriptable

In [3]: unique_archive.count(1)
Traceback (most recent call last):

  Cell In[3], line 1
    unique_archive.count(1)

AttributeError: 'set' object has no attribute 'count'


In [4]: unique_archive.index(1)
Traceback (most recent call last):

  Cell In[4], line 1
    unique_archive.index(1)

AttributeError: 'set' object has no attribute 'index'

However has the Collection data model methods __len__, __contains__ and __iter__:

In [5]: len(unique_archive)
Out[5]: 4
In [6]: 1 in unique_archive
Out[6]: True
In [7]: for unique_element in unique_archive:
      :     print(unique_element)
2
1
Γεια
3.14

Although the set has no numeric index it can still be enumerated. Each unique element in the set is randomly assigned an ordered numeric value:

In [8]: list(enumerate(unique_archive))
Out[8]: [(0, 2), (1, 1), (2, 'Γεια'), (3, 3.14)]

The printed formal representation, shows the preferred way to instantiate a set. Braces {} are used to enclose the Collection of elements where each eleemnt is a unique object:

In [9]: unique_archive
Out[9]: {1, 2, 3.14, 'Γεια'}

The braces {} are also used to enclose the Collection of elements for the dict. In the dict each element is an item which has a key value pair. Note that empty braces instantiates an empty dict and not an empty set:

In [10]: mapping = {}
Variable Explorer
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
unique_archive set 4 {2, 1, 'Γεια', 3.14}
mapping dict 0 {}

To instantiate an empty set, the class is used:

In [11]: empty_unique_archive = set()
Variable Explorer
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
unique_archive set 4 {2, 1, 'Γεια', 3.14}
mapping dict 0 {}
empty_unique_archive set 0 {}

An immutable set can be instantiated from the mutable set:

In [12]: immutable_unique_archive = frozenset(unique_archive)
Variable Explorer
archive tuple 8 ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
unique_archive set 4 {2, 1, 'Γεια', 3.14}
mapping dict 0 {}
empty_unique_archive set 0 {}
frozenset_unique_archive frozenset 4 frozenset({2, 1, 'Γεια', 3.14})

Note that the mutable set is much more commonly used and the printed formal representation of the immutable frozenset shows that an immutable frozenset is instantiated by casting a mutable set:

In [13]: immutable_unique_archive
Out[13]: frozenset({2, 1, 'Γεια', 3.14})

If the following sets are examined:

In [14]: exit
In [1]: unique1 = {0, 1, 2, 3, 4, 5, 6, 7, 8}
      : unique2 = {0, 1, 2}
      : unique3 = {7, 8, 9}
      : unique4 = {'a', 'b', 'c'}
Variable Explorer
unique1 set 9 {0, 1, 2, 3, 4, 5, 6, 7, 8}
unique2 set 3 {0, 1, 2}
unique3 set 3 {7, 8, 9}
unique4 set 3 {'a', 'b', 'c'}

The following comparison set methods issubset, issuperset and isdisjoint return a bool. unique2 is a subset of another unique1 because it unique1 contains all the elements in unique2:

In [2]: unique2.issubset(unique1)
Out[2]: True

unique3 is not a subset of another unique1 because unique3 contains the element 9 not in unique1:

In [3]: unique3.issubset(unique1)
Out[3]: False

Because unique1 contains all the elements of unique2 it is a superset:

In [3]: unique1.issubset(unique2)
Out[3]: True

If a set is equivalent to another set, it is classified as both a subset and superset:

In [4]: unique1.issubset(unique1)
Out[4]: True
In [5]: unique1.issubset(unique1)
Out[5]: True

A set is disjointed from another set if there are no overlapping elements:

In [6]: unique2.isdisjoint(unique4)
Out[6]: True
In [7]: unique2.isdisjoint(unique3)
Out[7]: True
In [8]: unique3.isdisjoint(unique1)
Out[8]: False

The comparison operators are setup for similar comparisons. The >= is essentially equivalent to issuperset and <= is essentially issuperset. The == will check for equality and != will check for differences. The > will only return True when one set is a superset that has additional elements. The < will only return True when one set is a subset that has additional elements.

In [3]: unique2 > unique1
Out[3]: True

In [4]: unique1 < unique2
Out[4]: True

In [5]: unique1 > unique1
Out[5]: False

In [6]: unique1 >= unique1
Out[6]: True

In [7]: unique2 == unique3
Out[7]: False

In [8]: unique2 != unique3
Out[8]: True

The following immutable set operations union, intersection, difference and symmetric_difference return a set that is the union (all unique elements in the set self and the set value), intersection (all elements present in both the set self and the set value), difference (elements in the set self that is not in the set value), symmetric_difference (elements not in the intersection):

In [9]: unique5 = {0, 1, 2, 3, 4, 5, 6}
      : unique6 = {4, 5, 6, 7, 8, 9}

It is insightful to conceptualise the above as:

In [9]: unique5 = {0, 1, 2, 3, 4, 5, 6}
      : unique6 =             {4, 5, 6, 7, 8, 9}
In [10]: unique5.union(unique6)
Out[10]: {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

In [11]: unique5.intersection(unique6)
Out[11]: {4, 5, 6}

In [12]: unique5.difference(unique6)
Out[12]: {0, 1, 2, 3}

In [13]: unique5.symmetric_difference(unique6)
Out[13]: {0, 1, 2, 3, 7, 8, 9}

The data model methods __or__, __and__, __sub__ and __xor__ define the behaviour of the |, &, - and ^ operators which have consistent behaviour to union, intersection, difference and symmetric_difference:

In [14]: unique5 | unique6
Out[14]: {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

In [15]: unique5 & unique6
Out[15]: {4, 5, 6}

In [16]: unique5 - unique6
Out[16]: {0, 1, 2, 3}

In [17]: unique5 ^ unique6
Out[17]: {0, 1, 2, 3, 7, 8, 9}

Each of these immutable set operations have a mutable counterpart update (union update), intersection_update, difference_update and symmetric_difference_update which update (mutate) the set in place:

Variable Explorer
unique5 set 7 {0, 1, 2, 3, 4, 5, 6}
unique6 set 6 {4, 5, 6, 7, 8, 9}
In [18]: unique5.update(unique6)
Variable Explorer
unique5 set 10 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
unique6 set 6 {4, 5, 6, 7, 8, 9}
In [19]: unique5.difference_update(unique6)
Variable Explorer
unique5 set 4 {0, 1, 2, 3}
unique6 set 6 {4, 5, 6, 7, 8, 9}
In [20]: unique5.symmetric_difference_update(unique6)
Variable Explorer
unique5 set 10 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
unique6 set 6 {4, 5, 6, 7, 8, 9}
In [21]: unique5.intersection_update(unique6)
Variable Explorer
unique5 set 6 {4, 5, 6, 7, 8, 9}
unique6 set 6 {4, 5, 6, 7, 8, 9}

The data model methods __ior__, __iand__, __isub__ and __ixor__ define the behaviour of the |=, &=, -= and ^= operators which have consistent behaviour to update (union update), intersection_update, difference_update and symmetric_difference_update. Returning to:

In [22]: unique5 = {0, 1, 2, 3, 4, 5, 6}
       : unique6 = {4, 5, 6, 7, 8, 9}
Variable Explorer
unique5 set 7 {0, 1, 2, 3, 4, 5, 6}
unique6 set 6 {4, 5, 6, 7, 8, 9}
In [23]: unique5 |= unique6
Variable Explorer
unique5 set 10 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
unique6 set 6 {4, 5, 6, 7, 8, 9}
In [24]: unique5 -= unique6
Variable Explorer
unique5 set 4 {0, 1, 2, 3}
unique6 set 6 {4, 5, 6, 7, 8, 9}
In [25]: unique5 ^ unique6
Variable Explorer
unique5 set 10 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
unique6 set 6 {4, 5, 6, 7, 8, 9}
In [26]: unique5 & unique6
Variable Explorer
unique5 set 6 {4, 5, 6, 7, 8, 9}
unique6 set 6 {4, 5, 6, 7, 8, 9}

The set also has the mutable methods remove and pop which behave consistently to the list methods remove and pop. Note as the set is unordered and has no numeric index, pop will pop off a random value:

In [27]: unique5.remove(4)
Variable Explorer
unique5 set 5 {5, 6, 7, 8, 9}
unique6 set 6 {4, 5, 6, 7, 8, 9}
In [28]: unique5.pop()
Out[28]: 5
Variable Explorer
unique5 set 4 {6, 7, 8, 9}
unique6 set 6 {4, 5, 6, 7, 8, 9}

Note that pop is a unique method that mutates the set in place and returns a value.

dict Collection

The dict is a Collection of items, where each item is an unique immutable key (typically but not always a str), that maps to a value that can be any object (the object mapped to can be immutable or mutable). The dict is not a Sequence but a Mapping. In a Mapping the data model method __getitem__ retrieves a value from a key opposed to a integer index like in a Sequence. items in a Mapping are internally ordered via item insertion order.

The dict is typically used to group multiple identifiers, this is best seen by an example:

In [29]: exit
In [1]: from matplotlib.colors import CSS4_COLORS

The CSS4_COLORS dict can be seen in the Variable Explorer:

Variable Explorer
CSS4_COLORS dict 148 {'aliceblue': '#F0F8FF', 'antiquewhite': '#FAEBD7', 'aqua': '#00FFFF', 'a...

This can be expanded:

CSS4_COLORS - dict (148 elements)
Key Type Size Value
0 aliceblue str 7 #F0F8FF
1 antiquewhite str 7 #FAEBD7
2 aqua str 7 #00FFFF
3 aquamarine str 7 #7FFFD4
4 azure str 7 #F0FFFF
5 beige str 7 #F5F5DC
6 bisque str 7 #FFE4C4
7 black str 7 #000000
8 blanchedalmond str 7 #FFEBCD
9 blue str 7 #0000FF

In this example, each key is a str which can be conceptualised as an identifier name, that is compartmentalised within the main name space under the name CSS4_COLORS. If the analogy of a directory is used, then CSS4_COLORS can be thought of as a subdirectory, grouping together all the identifiers corresponding to colors. Note that the key (identifier name) is an easy to remember name for each color and is a valid identifier name.

In [2]: all([key.isidentifier() for key in CSS4_COLORS])
Out[2]: True

The value on the other hand is a hexadecimal str of length 7. If aliceblue is examined for example the hexadecimal string value is retrieved from the dict CSS4_COLORS using its key 'aliceblue':

In [3]: CSS4_COLORS['aliceblue']
Out[3]: '#F0F8FF'

This hexadecimal value is three bytes:

In [4]: 0xF0, 0xF8, 0xFF
Out[4]: (240, 248, 255)

Python typically prefers lowercase for hexadecimal values:

In [5]: hex(240), hex(248), hex(255)
Out[5]: ('0xf0', '0xf8', '0xff')

The color picker in Microsoft Paint can be used to visualise the color CSS4_COLORS['aliceblue']:

img_001

A computer screen, can be considered as a matrix of pixels. In each pixel is a Red, Blue, Green Light Emitting Diode (RGB LED) and each color in the RGB LED has a variable intensity level spanning over 1 byte (0:256). An RGB LED can be examined using an Arduino:

img_002

TinkerCad aliceblue RGB LED

Another color dict is BASE_COLORS:

In [6]: from matplotlib.colors import BASE_COLORS

Which expresses the primary base colors (blue, green and red), secondary base colors (cyan, magenta, yellow) absense of color (black) and all LEDs at maximum brightness (white) as a normalised 3 element tuple:

Variable Explorer
CSS4_COLORS dict 148 {'aliceblue': '#F0F8FF', 'antiquewhite': '#FAEBD7', 'aqua': '#00FFFF', 'a...
BASE_COLORS dict 8 {'b': (0, 0, 1), 'g': (0, 0.5, 0), 'r': (1, 0, 0), 'c': (0, 0.75, 0.75),...

This can be expanded:

BASE_COLORS - dict (8 elements)
Key Type Size Value
0 b tuple 3 (0, 0, 1)
1 g tuple 3 (0, 0.5, 0)
2 r tuple 3 (1, 0, 0)
3 c tuple 3 (0, 0.75, 0.75)
4 m tuple 3 (0.75, 0, 0.75)
5 y tuple 3 (0.75, 0.75, 0)
6 k tuple 3 (0, 0, 0)
7 w tuple 3 (1, 1, 1)

Note the human eye is about as twice as sensitive to green as red and blue, so a slight correction factor has been made in the values to compensate for this.

A simplified dict instance color_mapping can be instantiated using:

In [7]: exit
In [1]: color_mapping = {'red': '#ff0000', 'green': '#008000', 'blue': '#0000ff'}

This can be viewed on the Variable Explorer:

Variable Explorer
color_mapping dict 3 {'red': '#ff0000', 'green': '#008000', 'blue': '#0000ff'}

And can be expanded:

color_mapping - dict (3 elements)
Key Type Size Value
0 red str 7 #ff0000
1 green str 7 #008000
2 blue str 7 #0000ff

Because this dict instance only has three items, it can be instantiated on a single line. For dict instances with more items, it is more common to display each item on a new line:

In [2]: color_mapping2 = {'red': '#ff0000', 
      :                   'green': '#008000', 
      :                   'blue': '#0000ff'}
Variable Explorer
color_mapping dict 3 {'red': '#ff0000', 'green': '#008000', 'blue': '#0000ff'}
color_mapping2 dict 3 {'red': '#ff0000', 'green': '#008000', 'blue': '#0000ff'}

Recall that each key is a str which can be conceptualised as an identifier name belonging to the namespace of the dict instance color_mapping:

In [3]: for key in color_mapping:
      :     print(key, key.isidentifier())
red True
green True
blue True

When each key is a valid identifier name, the dict class can also be used to instantiate a dict instance:

In [4]: color_mapping3 = dict(red='#ff0000', green='#008000', blue='#0000ff')

Notice that each key is provided as a named parameter assigned to a value. This form is often used to compartmentalise parameters in more complicated function calls which can be seen in the code below.

Notice the axes annotate method compartmentalises the input parameters that define the arrow properties into arrowprops which is supplied as a dict. When the dict class is explicitly used, the syntax highlighting is consistent making the code easier to read:

In [5]: import matplotlib.pyplot as plt
      : x = [0, 1, 2, 3, 4, 5]
      : y = [0, 2, 4, 6, 8, 10]
      : fig, ax = plt.subplots()
      : ax.plot(x, y, color='tomato')
      : ax.annotate(text='customarrow', 
      :             xy=(4, 8), 
      :             xytext=(4, 6), 
      :             xycoords='data', 
      :             arrowprops=dict(facecolor='royalblue', 
      :                             edgecolor='skyblue', 
      :                             headlength=10, 
      :                             headwidth=10, 
      :                             linewidth=2)
      :            )
Out[5]: Text(4, 6, 'customarrow')

img_003

A dict can be instantiated by zipping two other Collections:

In [5]: keys4 = ('red', 'green', 'blue')
      : values4 = ('#ff0000', '#008000', '#0000ff')

In [6]: zip(keys4, values4)
Out[6]: <zip at 0x1edacfc5980>

In [7]: list(zip(keys4, values4))
Out[7]: [('red', '#ff0000'), ('green', '#008000'), ('blue', '#0000ff')]

In [8]: dict(zip(keys4, values4))
Out[8]: {'red': '#ff0000', 'green': '#008000', 'blue': '#0000ff'}

In [9]: color_mapping4 = dict(zip(keys4, values4))

The dict instance has the attributes items, keys and values which are each a Collection:

In [10]: color_mapping.items()
Out[10]: dict_items([('red', '#ff0000'), ('green', '#008000'), ('blue', '#0000ff')])

In [11]: color_mapping.keys()
Out[11]: dict_keys(['red', 'green', 'blue'])

In [12]: color_mapping.values()
Out[12]: dict_values(['#ff0000', '#008000', '#0000ff'])

Notice that dict_items is similar to enumeration of a Collection that has a numeric index:

In [13]: list(enumerate(values4))
Out[13]: [(0, '#ff0000'), (1, '#008000'), (2, '#0000ff')]

These Collection attributes are normally used for the purpose of looping:

In [14]: for key, value in color_mapping.items():
       :     print(key, value)
       :
red #ff0000
green #008000
blue #0000ff        
In [15]: for key in color_mapping.keys():
       :     print(key)
       :
red
green
blue        
In [16]: for value in color_mapping.values():
       :     print(value)
       :
#ff0000
#008000
#0000ff        

The Collection based data model methods of the dict, are based on the keys. The data model method __iter__ which defines the behaviour when casting to the iter class:

In [17]: forward = iter(color_mapping)
In [18]: next(forward)
Out[18]: 'red'

In [19]: next(forward)
Out[19]: 'green'

In [20]: next(forward)
Out[20]: 'blue'

Therefore the keys attribute is not commonly used as looping over a dict, loops over the keys:

In [21]: for key in color_mapping:
       :     print(key)
       :
red
green
blue        

The data model method __contains__ checks if a key is in a dict:

In [22]: 'red' in color_mapping
Out[22]: True

The data model method __len__ returns the number of keys in a dict which is consistent to the number of items and number of values:

In [23]: len(color_mapping)
Out[23]: 3

The data model method __getitem__ allows indexing of a dict using the key to return the value:

In [24]: color_mapping['red']
       : '#ff0000'

Note only one key can be indexed at a time:

In [25]: color_mapping['red', 'green']
Traceback (most recent call last):

  Cell In[25], line 1
    color_mapping['red', 'green']

KeyError: ('red', 'green')

This is because a key can be any immutable object such as a tuple and therefore using __getitem__ above looks for the key ('red', 'green'). The following is an example of using tuple keys:

In [26]: color_mapping_r = {(1, 0, 0): 'red', (0, 0.5, 0): 'green', (0, 0, 1): 'blue'}
Variable Explorer
color_mapping_r dict 3 {(1, 0, 0): 'red', (0, 0.5, 0): 'green', (0, 0, 1): 'blue'}
color_mapping_r - dict (3 elements)
Key Type Size Value
0 (1, 0, 0) str 3 red
1 (0, 0.5, 0) str 5 green
2 (0, 0, 1) str 4 blue

Looping over a dict uses the keys and the data model method __getitem__ can be used to retrieve a value corresponding to the key:

In [27]: for key in color_mapping:
       :     value = color_mapping[key]
       :     print(key, value)
       :
red #ff0000
green #008000
blue #0000ff        
In [28]: for key in color_mapping:
       :     print(key, color_mapping[key])
       :
red #ff0000
green #008000
blue #0000ff        

Only the is equal to == and not equal to != comparison operators are setup for a dict:

In [29]: color_mapping == color_mapping2
Out[29]: True

In [30]: color_mapping != color_mapping_r
Out[30]: True

Recall that a mutable Collection can be equal to another mutable Collection but not the same mutable Collection:

In [31]: color_mapping is color_mapping2
Out[31]: False

The dict has the method copy which can be used to create a shallow copy:

In [32]: id(color_mapping_r)
Out[32]: 2120354036224

In [33]: color_mapping_r2 = color_mapping_r.copy()

In [34]: id(color_mapping_r2)
Out[34]: 2120354020800
Variable Explorer
color_mapping_r dict 3 {(1, 0, 0): 'red', (0, 0.5, 0): 'green', (0, 0, 1): 'blue'}
color_mapping_r2 dict 3 {(1, 0, 0): 'red', (0, 0.5, 0): 'green', (0, 0, 1): 'blue'}

The mutable method clear will clear all items from a dict:

In [35]: color_mapping_r2.clear()
Variable Explorer
color_mapping_r dict 3 {(1, 0, 0): 'red', (0, 0.5, 0): 'green', (0, 0, 1): 'blue'}
color_mapping_r2 dict 0 {}

The dict has the mutable method popitem which pops off the last item, returing the item as a tuple and mutating the dict instance in place:

In [36]: color_mapping_r.popitem()
Out[36]: ((0, 0, 1), 'blue')
Variable Explorer
color_mapping_r dict 2 {(1, 0, 0): 'red', (0, 0.5, 0): 'green'}
color_mapping_r2 dict 0 {}

And the mutable method pop which pops off an item, using its key returning the value and mutating the dict instance in place:

In [37]: color_mapping_r.pop('red')
Out[37]: (1, 0, 0)
Variable Explorer
color_mapping_r dict 1 {(0, 0.5, 0): 'green'}
color_mapping_r2 dict 0 {}

The __getitem__ method will flag up a KeyError when a key does not exist:

Variable Explorer
color_mapping dict 3 {'red': '#ff0000', 'green': '#008000', 'blue': '#0000ff'}

The key 'yellow' does not exist for example:

In [38]: color_mapping['yellow']
Traceback (most recent call last):

  Cell In[38], line 1
    color_mapping['yellow']

KeyError: 'yellow'

The method get can be used to get a value using the key, returning None if the key does not exist:

In [39]: color_mapping.get('red')
Out[39]: '#ff0000'
In [40]: color_mapping.get('yellow')
In [41]:

The method setdefault can be used to retrieve a value using a key if the key exists as seen in Out [41] otherwise when the key does not exist as in In [42] the method will create a new item using the key and a provided default value mutating the dict in place and provide this default value as seen in Out [42]:

In [41]: color_mapping.setdefault('red', '#000000')
Out[41]: '#ff0000'

In [42]: color_mapping.setdefault('yellow', '#000000')
Out[42]: '#000000'
Variable Explorer
color_mapping dict 3 {'red': '#ff0000', 'green': '#008000', 'blue': '#0000ff', 'yellow': '#0000ff'}

The mutable method __setitem__ can be used to update the value corresponding to a key:

In [43]: color_mapping['yellow'] = '#bfbf00'
Variable Explorer
color_mapping dict 3 {'red': '#ff0000', 'green': '#008000', 'blue': '#0000ff', 'yellow': '#bfbf00'}

It can also be used to create a new item:

In [44]: color_mapping['cyan'] = '#00bfbf'
Variable Explorer
color_mapping dict 4 {'red': '#ff0000', 'green': '#008000', 'blue': '#0000ff', 'yellow': '#bfbf00', 'cyan': '#00bfbf'}

The mutable method __delitem__ can be used to delete an item using the key:

In [45]: del color_mapping['yellow']
Variable Explorer
color_mapping dict 3 {'red': '#ff0000', 'green': '#008000', 'blue': '#0000ff', 'cyan': '#00bfbf'}

The mutable method update can be used to update a dict in place using another dict:

In [46]: color_mapping.update({'yellow': (0.75, 0.75, 0), 'cyan': (0, 0.75, 0.75), 'magenta': (0.75, 0, 0.75)})
Variable Explorer
color_mapping dict 3 {'red': '#ff0000', 'green': '#008000', 'blue': '#0000ff', 'cyan': (0, 0.75, 0.75), 'yellow': (0.75, 0.75, 0), 'magenta': (0.75, 0, 0.75)}

Notice the insertion order above, the key 'cyan' was in color_mapping before the update and is updated in place. The keys 'yellow' and 'magenta' were not present in color_mapping and were added.

The data model method __or__ defines the behaviour of the | operator and is the immutable counterpart to update, returning an updated dict instance:

In [47]: color_mapping | {'cyan': '#00bfbf', 'yellow': '#bfbf00', 'magenta': '#bf00bf', 'black': '#000000', 'white': '#ffffff'}
Out[47]: 
{'red': '#ff0000',
 'green': '#008000',
 'blue': '#0000ff',
 'cyan': '#00bfbf',
 'yellow': '#bfbf00',
 'magenta': '#bf00bf',
 'black': '#000000',
 'white': '#ffffff'}

The data model __ior__ defines the behaviour of the mutable coutnerpart |= and gives consistent behaviour to the update method:

In [48]: color_mapping |= {'cyan': '#00bfbf', 'yellow': '#bfbf00', 'magenta': '#bf00bf', 'black': '#000000', 'white': '#ffffff'}
Variable Explorer
color_mapping dict 8 {'red': '#ff0000', 'green': '#008000', 'blue': '#0000ff', 'cyan': '#00bfbf', 'yellow': '#bfbf00', 'magenta': '#bf00bf', 'black': '#000000', 'white': '#ffffff'}

collections Module

The collections module contains a number of supplementary Collection based classes which closely complement those from builtins. The module can be imported and its documentation examined:

In [49]: exit
In [1]: import collections
In [2]: collections?
Type:        module
String form: <module 'collections' from 'C:\\Users\\phili\\AppData\\Local\\spyder-6\\envs\\spyder-runtime\\Lib\\collections\\__init__.py'>
File:        c:\users\phili\appdata\local\spyder-6\envs\spyder-runtime\lib\collections\__init__.py
Docstring:  
This module implements specialized container datatypes providing
alternatives to Python's general purpose built-in containers, dict,
list, set, and tuple.
* namedtuple   factory function for creating tuple subclasses with named fields
* deque        list-like container with fast appends and pops on either end
* ChainMap     dict-like class for creating a single view of multiple mappings
* Counter      dict subclass for counting hashable objects
* OrderedDict  dict subclass that remembers the order entries were added
* defaultdict  dict subclass that calls a factory function to supply missing values
* UserDict     wrapper around dictionary objects for easier dict subclassing
* UserList     wrapper around list objects for easier list subclassing
* UserString   wrapper around string objects for easier string subclassing

Notice that the namedtuple is a factory function that allows essentially creation of a tuple with named fields.

The deque is a list-like class.

The ChainMap, Counter, OrderedDict and defaultdict are dict subclasses.

  • The defaultdict is a dict that carries out additional instructions when a key is missing. This dict has the data model method __missing__ which is used to generate a value when __getitem__ is used with an unknown key. The data model __missing__ can be thought of as an automatic implementation of setdefault.
  • The Counter is used to count the number of repeated values in a Collection.
  • OrderedDict is essentially obsolete as dict was updated to have consistent behaviour. In older versions of Python, the dict class behaved more like a set.

The UserString, UserList and UserDict are classes that are typically used for the purposes of subclassing.

namedtuple

If a blank figure is created:

In [3]: import matplotlib.pyplot as plt
In [4]: fig = plt.figure()

img_004

A set of axes can be added to it using the figure add_axes method. The docstring of this method can be examined, notice it has the field rect:

In [5]: fig.add_axes?
Signature: fig.add_axes(*args, **kwargs)
Docstring:
Add an `~.axes.Axes` to the figure.

Call signatures::

    add_axes(rect, projection=None, polar=False, **kwargs)
    add_axes(ax)

Parameters
----------
rect : tuple (left, bottom, width, height)
    The dimensions (left, bottom, width, height) of the new
    `~.axes.Axes`. All quantities are in fractions of figure width and
    height.

If this rect is supplied as a tuple:

In [6]: fig.add_axes(rect=(0.2, 0.3, 0.4, 0.5))
Out[6]: <Axes: >

Note that the rectange displays on the figure as expected:

img_005

The line of code above is not very readible as it is not immediately clear what each number represents. Using a dict for example would be more readible`:

In [7]: fig.add_axes(rect=dict(left=0.2, bottom=0.3, width=0.4, height=0.5))

And as this function expects a tuple the following could be used:

In [7]: fig.add_axes(rect=tuple(dict(left=0.2, bottom=0.3, width=0.4, height=0.5).values()))

namedtuple is a factory function, that can be used to create a NamedTuple child class with field names. In the example above:

In [8]: from collections import namedtuple
In [9]: RectTuple = namedtuple('RectTuple', 
      :                        field_names=['left', 'bottom', 'width', 'height'], 
      :                        defaults=(0, 0, 1, 1))

Instances of this class can then be instantiated:

In [10]: rect1 = RectTuple(left=0.2, bottom=0.3, width=0.4, height=0.5)
In [11]: rect2 = RectTuple(bottom=0.3, width=0.4, height=0.5)
In [12]: rect3 = RectTuple(width=0.4, height=0.5, left=0.2, bottom=0.3)

These display on the Variable Explorer as:

Variable Explorer
Name ▲ Type Size Value
rect1 RectTuple 4 RectTuple(left=0.2, bottom=0.3, width=0.4, height=0.5)
rect2 RectTuple 4 RectTuple(left=0, bottom=0.3, width=0.4, height=0.5)
rect3 RectTuple 4 RectTuple(left=0.2, bottom=0.3, width=0.4, height=0.5)

And when rect1 is expanded:

rect1 - RectTuple (4 elements)
Index ▲ Field Name Type Size Value
0 left int 1 0.2
1 bottom int 1 0.3
2 width int 1 0.4
3 height int 1 0.5

Because the RectTuple is a child classes of the tuple class, these instances are also effectively tuple instances:

Variable Explorer
Name ▲ Type Size Value
rect1 tuple 4 (0.2, 0.3, 0.4, 0.5)
rect2 tuple 4 (0, 0.3, 0.4, 0.5)
rect3 tuple 4 (0.2, 0.3, 0.4, 0.5)

When the tuple instance rect1 is expanded:

rect1 - tuple (4 elements)
Index ▲ Type Size Value
0 int 1 0.2
1 int 1 0.3
2 int 1 0.4
3 int 1 0.5

When the RectTuple instance is supplied to a function that expects a tuple, it will be cast to the tuple above. Now:

In [13]: fig = plt.figure()
       : fig.add_axes(rect=RectTuple(0.2, 0.3, 0.4, 0.5), facecolor='royalblue')
       : fig.add_axes(rect=RectTuple(0.3, 0.4, 0.2, 0.2), facecolor='tomato')
Out[13]: <Axes: >

img_006

The RectTuple is a child class of the tuple class and inherits the attributes and methods from a tuple. As these are inherited and not redefined, their behaviour is identical. The RectTuple also has a handful of additional attributes and methods not available to a tuple:

In [14]: rect1.left
Out[14]: 0.2

In [15]: rect1.bottom
Out[15]: 0.3

In [16]: rect1.width
Out[16]: 0.4

In [17]: rect1.height
Out[17]: 0.5

The _fields attribute is a tuple of field names:

In [18]: rect1._fields
Out[18]: ('left', 'bottom', 'width', 'height')

The _field_defaults attribute is a dict where the keys are the field names and the values are the default values:

In [19]: rect1._field_defaults
Out[19]: {'left': 0, 'bottom': 0, 'width': 1, 'height': 1}

The _as_dict method casts the RectTuple instance to a dict where the keys are the field names and the values are the values:

In [20]: rect1._asdict()
Out[20]: {'left': 0.2, 'bottom': 0.3, 'width': 0.4, 'height': 0.5}

The _make class method is an alternative constructor that can be used to construct the RectTuple from a tuple:

In [21]: RectTuple._make((0.1, 0.2, 0.3, 0.4))
Out[21]: RectTuple(left=0.1, bottom=0.2, width=0.3, height=0.4)

The _replace method can be used to return a new instance, where one or more of the fields are updated:

In [22]: rect1._replace(bottom=0.4, width=0.2)
Out[22]: RectTuple(left=0.2, bottom=0.4, width=0.2, height=0.5)
In [23]: exit

deque Collection

The double-ended queue deque is a list-like class, which was originally designed to be a builtins class before being compartmentalised in the builtins module. As it was designed to be a builtins, it has a lowercase class name. The deque class is typically imported from Collections:

In [1]: from collections import deque

Its identifiers can be examined:

In [2]: dir(deque)
Out[2]: ['__add__', '__class__', '__class_getitem__', '__contains__', '__copy__', 
         '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', 
         '__ge__', '__getattribute__', '__getitem__', '__getstate__', '__gt__', 
         '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', 
         '__iter__', '__le__', '__len__', '__lt__', '__module__', '__mul__', 
         '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', 
         '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', 
         '__str__', '__subclasshook__', 'append', 'appendleft', 'clear', 'copy', 
         'count', 'extend', 'extendleft', 'index', 'insert', 'maxlen', 'pop', 
         'popleft', 'remove', 'reverse', 'rotate']

Notice that most of these identifiers are consistent to those in the list class:

In [3]: deque.
# ------------------------------------
# Available Identifiers for `deque`:
# ------------------------------------

# 🔧 Functions from `object` (inherited by `deque`):
#     - __init__(self, iterable=None, maxlen=None, /)  : Initializes the deque with optional items and maxlen.
#     - __new__(*args, **kwargs)                       : Creates a new instance of the class.
#     - __delattr__(self, name, /)                     : Defines behavior for when an attribute is deleted.
#     - __dir__(self, /)                               : Default dir() implementation.
#     - __sizeof__(self, /)                            : Returns the size of the object in memory, in bytes.
#     - __repr__(self, /)                              : Returns a string representation of the deque.
#     - __str__(self, /)                               : Returns a string for display purposes.
#     - __format__(self, format_spec, /)               : Returns a formatted string representation of the object.
#     - __hash__(self, /)                              : Returns a hash of the object (Not implemented for `deque`).
#     - __getattribute__(self, name, /)                : Gets an attribute from the object.
#     - __setattr__(self, name, value, /)              : Sets an attribute on the object.
#     - __delattr__(self, name, /)                     : Deletes an attribute from the object.
#     - __reduce__(self, /)                            : Prepares the object for pickling.
#     - __reduce_ex__(self, protocol, /)               : Similar to __reduce__, with a protocol argument.

# 🔍 Attributes from `object`:
#     - __class__                                      : The class of the deque object.
#     - __doc__                                        : The docstring of the deque class.

# 🔧 Collection-Based Methods (from `deque` and Collection ABC):
#     - __contains__(self, item, /)                    : Checks if an item is in the deque (`in`).
#     - __iter__(self, /)                              : Returns an iterator over the deque.
#     - __len__(self, /)                               : Returns the length of the deque.
#     - __getitem__(self, index, /)                    : Retrieves an item by index (`[]`).
#     - count(self, value, /)                          : Counts the occurrences of a value in the deque.

# 🔧 Mutable Collection-Specific Methods:
#     - __setitem__(self, index, value, /)             : Sets the value of an item by index.
#     - __delitem__(self, index, /)                    : Deletes an item by index.
#     - append(self, item, /)                          : Appends an item to the right end of the deque.
#     - appendleft(self, item, /)                      : Appends an item to the left end of the deque.
#     - clear(self, /)                                 : Removes all items from the deque.
#     - extend(self, iterable, /)                      : Appends all items from an iterable to the right end.
#     - extendleft(self, iterable, /)                  : Appends all items from an iterable to the left end.
#     - pop(self, /)                                   : Removes and returns an item from the right end.
#     - popleft(self, /)                               : Removes and returns an item from the left end.
#     - remove(self, value, /)                         : Removes the first occurrence of a value.
#     - reverse(self, /)                               : Reverses the order of the deque in place.
#     - rotate(self, n=1, /)                           : Rotates the deque n steps to the right (or left if n < 0).

# 🔍 Attributes:
#     - maxlen                                         : The maximum size of the deque, or None if unlimited.

When a list is written on a single line:

In [4]: ['a', 'b', 'c', 'd', 'e']

The list methods append, extend, pop operate on the right-hand side of the list. Because the deque is double-ended, mutable methods like append, extend, pop have a left counterpart appendleft, extendleft, popleft. The deque can also be instantiated with a maximum length maxlen:

In [5]: double_ended_mutable_archive = deque(['a', 'b', 'c', 'd', 'e'], maxlen=7)
Variable Explorer
Name ▲ Type Size Value
double_ended_mutable_archive deque 5 deque(['a', 'b', 'c', 'd', 'e'], maxlen=7)

The mutable method rotate, will rotate each element by a specified integer number of places. A rotation by 1 effectively pops an element from the right of the deque and then left appends the popped element deque. A rotation of 2 does this twice:

In [6]: double_ended_mutable_archive.rotate(2)
Variable Explorer
Name ▲ Type Size Value
double_ended_mutable_archive deque 5 deque(['d', 'e', 'a', 'b', 'c'], maxlen=7)

A rotation by -1 effectively pops an element from the left of the deque and then right appends the popped element deque. A rotation of -4 does this four times:

In [7]: double_ended_mutable_archive.rotate(-4)
Variable Explorer
Name ▲ Type Size Value
double_ended_mutable_archive deque 5 deque(['c', 'd', 'e', 'a', 'b'], maxlen=7)

The mutable method append, appends a single element to the right of the deque:

In [9]: double_ended_mutable_archive.append('x')
Variable Explorer
Name ▲ Type Size Value
double_ended_mutable_archive deque 6 deque(['c', 'd', 'e', 'a', 'b', 'x'], maxlen=7)

The mutable method appendleft, appends a single element to the left of the deque:

In [10]: double_ended_mutable_archive.appendleft('y')
Variable Explorer
Name ▲ Type Size Value
double_ended_mutable_archive deque 7 deque(['y', 'c', 'd', 'e', 'a', 'b', 'x'], maxlen=7)

The deque now is at its maximum length. Appending another element, will eject an element from the other end of the deque:

In [11]: double_ended_mutable_archive.appendleft('z')
Variable Explorer
Name ▲ Type Size Value
double_ended_mutable_archive deque 7 deque(['z', 'y', 'c', 'd', 'e', 'a', 'b'], maxlen=7)

The element 'x' will be discarded and is not returned because the append and appendleft methods have no return value.

The mutable methods extend and extendleft can be used to extend the deque using another Collection from the right and left respectively:

In [12]: double_ended_mutable_archive.extendleft('hello')
Variable Explorer
Name ▲ Type Size Value
double_ended_mutable_archive deque 7 deque(['o', 'l', 'l', 'e', 'h', 'z', 'y'], maxlen=7)
In [13]: double_ended_mutable_archive.extend('abc')
Variable Explorer
Name ▲ Type Size Value
double_ended_mutable_archive deque 7 deque(['e', 'h', 'z', 'y', 'a', 'b', 'c'], maxlen=7)

The mutable methods pop and popleft can be used to pop an element from the right and left of the deque respectively:

In [14]: double_ended_mutable_archive.pop()
Out[14]: 'c'
Variable Explorer
Name ▲ Type Size Value
double_ended_mutable_archive deque 6 deque(['e', 'h', 'z', 'y', 'a', 'b'], maxlen=7)
In [15]: double_ended_mutable_archive.pop()
Out[15]: 'e'
Variable Explorer
Name ▲ Type Size Value
double_ended_mutable_archive deque 5 deque(['h', 'z', 'y', 'a', 'b'], maxlen=7)

The deque is commonly used for a cache. For example if a cache of the last three websites visited is made:

In [16]: website_cache = deque([], maxlen=3)
Variable Explorer
Name ▲ Type Size Value
website_cache deque 0 deque([], maxlen=3)
In [17]: website_cache.appendleft('https://www.python.org/')
Variable Explorer
Name ▲ Type Size Value
website_cache deque 1 deque(['https://www.python.org/'], maxlen=3)
In [18]: website_cache.appendleft('https://numpy.org/')
Variable Explorer
Name ▲ Type Size Value
website_cache deque 2 deque(['https://numpy.org/', 'https://www.python.org/'], maxlen=3)
In [19]: website_cache.appendleft('https://matplotlib.org/')
Variable Explorer
Name ▲ Type Size Value
website_cache deque 3 deque(['https://matplotlib.org/', 'https://numpy.org/', 'https://www.python.org/'], maxlen=3)
In [20]: website_cache.appendleft('https://pandas.pydata.org/')
Variable Explorer
Name ▲ Type Size Value
website_cache deque 3 deque(['https://pandas.pydata.org/', 'https://matplotlib.org/', 'https://numpy.org/'], maxlen=3)

defaultdict Collection

When a key in a dict is looked up that doesn't exist, a KeyError is flagged up:

In [21]: exit
In [1]: color_mapping = {'red': '#ff0000', 
      :                  'green': '#008000',
      :                  'blue': '#0000ff'}
In [2]: color_mapping['yellow']
Traceback (most recent call last):

  Cell In[2], line 1
    color_mapping['yellow']

KeyError: 'yellow'

The defaultdict is a dict subclass which has the additional data model method __missing__ defined. For instantiation, a defaultfactory function is required alongside a dict. If the defaultfactory function is None, the instructions provided for __missing__ is None and the defaultdict therefore behaves identically to a dict:

In [3]: from collections import defaultdict
In [4]: color_mapping = defaultdict(None, 
      :                            {'red': '#ff0000', 
      :                             'green': '#008000',
      :                             'blue': '#0000ff'})
In [5]: color_mapping['yellow']
Traceback (most recent call last):

  Cell In[5], line 1
    color_mapping['yellow']

KeyError: 'yellow'

Recall that the str class is a callable that can be referenced In [6] and called using parenthesis In [7]:

In [6]: str
Out[6]: str

In [7]: str()
Out[7]: ''

The defaultfactory function can reference the str class, which is called when a key is missing via the data model method __missing__:

In [8]: color_mapping = defaultdict(str, 
      :                            {'red': '#ff0000', 
      :                             'green': '#008000',
      :                             'blue': '#0000ff'})
In [9]: color_mapping['yellow'] # color_mapping.__missing__('yellow')
Out[9]: ''

In [10]: color_mapping['cyan']
Out[10]: ''

Notice that the following items are appended to the defaultdict:

Variable Explorer
color_mapping defaultdict 5 defaultdict(str, {'red': '#ff0000', 'green': '#008000', 'blue': '#0000ff', 'yellow': '', 'cyan': ''}

The following function can be defined In [11], referenced In [12] and called In [13]:

In [11]: def zero_color():
       :     return '#000000'

In [12]: zero_color
       : <function __main__.zero_color()>

In [13]: zero_color()
       : '#000000'

This function can be used as the defaultfactory default function. However the function above is more commonly provided as a lambda expression (where the lambda keyword should be thought of as meaning make function). Recall that the lambda expression is essentially a shorthand way to write a function on a single line:

In [11]: def zero_color(None):
       :     return '#000000'

In [11]: zero_color = lambda None: '#000000'

There is no input argument so:

In [11]: def zero_color():
       :     return '#000000'

In [11]: zero_color = lambda: '#000000'

The lambda function is often used anonymously (so is not assigned to an object name):

In [11]: lambda: '#000000'

Anonymous functions can be referenced:

In [14]: lambda: '#000000'
Out[14]: <function __main__.<lambda>()>

In [15]: (lambda: '#000000')
Out[15]: <function __main__.<lambda>()>

And called:

In [16]: (lambda: '#000000')()
Out[16]: '#000000'

The defaultfactory function can reference the str class, which is called when a key is missing via the data model method __missing__:

In [17]: color_mapping = defaultdict(lambda: '#000000', 
       :                             {'red': '#ff0000', 
       :                              'green': '#008000',
       :                              'blue': '#0000ff'})
In [18]: color_mapping['yellow'] # color_mapping.__missing__('yellow')
Out[18]: '#000000'

In [19]: color_mapping['cyan']
Out[19]: '#000000'

Notice that the following items are appended to the defaultdict:

Variable Explorer
color_mapping defaultdict 5 defaultdict(str, {'red': '#ff0000', 'green': '#008000', 'blue': '#0000ff', 'yellow': '#000000', 'cyan': '#000000'}

Counter Collection

The Counter is a dict subclass that is used to count the number of repeated values in a Collection that can have duplicate elements. For example archive, text and text_b examined earlier:

In [20]: from collections import Counter
In [21]: archive = ('Γεια', 'Γεια', 'Γεια', 1, 1, True, 3.14, 2)
       : text = 'Γεια σου Κοσμο!'
       : text_b = bytes('Γεια σου Κοσμο!', encoding='utf-8')

Notice the cell output, which displays the printed formal representation shows the Counter class being instantiated from a dict. Note that the order of the keys in the Counter matches the order they were found in the original Collection when the elements have equal counts. When the elements have differing counts, they are reordering by most occuring to least occuring:

In [22]: Counter(archive)
Out[22]: Counter({'Γεια': 3, 1: 3, 3.14: 1, 2: 1})

In [23]: Counter(text)
Out[23]: 
Counter({'ο': 3,
         ' ': 2,
         'σ': 2,
         'Γ': 1,
         'ε': 1,
         'ι': 1,
         'α': 1,
         'υ': 1,
         'Κ': 1,
         'μ': 1,
         '!': 1})

In [24]: Counter(text_b)
Out[24]: 
Counter({206: 9,
         207: 3,
         191: 3,
         32: 2,
         131: 2,
         147: 1,
         181: 1,
         185: 1,
         177: 1,
         133: 1,
         154: 1,
         188: 1,
         33: 1})

If one of these is instantiated:

In [25]: counts = Counter(archive)
Variable Explorer
counts Counter 4 Counter({'Γεια': 3, 1: 3, 3.14: 1, 2: 1})

The Counter is a dict subclass and inherits all the methods in a dict. In a Counter, all the values are int instances and this allows additional numeric behaviour therefore a numeric operators and comparison operators are defined/redefined:

In [26]: Counter.
# -------------------------------
# Available Identifiers Redefined or Defined in `Counter`:
# -------------------------------------

# 🔧 Data Model Methods:
#     - __init__(self, iterable=None, /, **kwds)       : Initializes the `Counter` object with the given 
#                                                      : iterable or mapping (default is an empty Counter).
#     - __repr__(self, /)                              : Returns a string representation of the `Counter` 
#                                                      : object, showing elements and their counts.
#     - __reduce__(self, /)                            : Prepares the `Counter` object for pickling, returning 
#                                                      : the necessary state for re-creation.

# 🔍 Attributes (defined in `Counter`):
#     - __dict__                                      : The instance attribute dictionary for the `Counter` 
#                                                     : class, holding any custom attributes.
#     - __module__                                    : The module in which the `Counter` class is defined 
#                                                     : (`collections`).
#     - __weakref__                                   : A list of weak references to the `Counter` object, if
#                                                     : any.

# 🔧 Collection-Based Methods (redefined in `Counter`):
#     - __contains__(self, key, /)                     : Checks if an element exists in the Counter (returns 
#                                                      : False for elements with a count of 0).
#     - __getitem__(self, key, /)                      : Returns the count of the element; returns 0 if the 
#                                                      : key is missing.
#     - __delitem__(self, key, /)                      : Removes an element from the Counter (raises KeyError 
#                                                      : if not found).
#     - __missing__(self, key, /)                      : Returns 0 by default when the key is missing (for 
#                                                      : consistency in behavior with `Counter` objects).

# 🔄 Mapping-Specific Methods (redefined in `Counter`):
#     - keys(self, /)                                  : Returns a view of the Counter's keys (elements).
#     - values(self, /)                                : Returns a view of the Counter's counts.
#     - items(self, /)                                 : Returns a view of the Counter's items as (element, 
#                                                      : count) pairs.
#     - __fromkeys__(self, iterable, value=None, /)    : Creates a new `Counter` from the given iterable, 
#                                                      : setting all counts to the given value (or None by 
#                                                      : default).

# 🔄 Counter-Specific Methods (defined in `Counter`):
#     - update(self, /, *args, **kwargs)               : Adds counts from another iterable, mapping, or 
#                                                      : keyword arguments.
#     - subtract(self, /, *args, **kwargs)             : Subtracts counts from another iterable, mapping, or 
#                                                      : keyword arguments (allows negative counts).
#     - most_common(self, n=None, /)                   : Returns a list of the n most common elements and 
#                                                      : their counts, sorted by count.
#     - elements(self, /)                              : Returns an iterator over elements, repeating each as 
#                                                      : per its count (ignores elements with count ≤ 0).
#     - total(self, /)                                 : Returns the sum of all counts (sum(counter.values())).
#     - copy(self, /)                                  : Returns a shallow copy of the `Counter` object.

# 🔄 Binary Operators (redefined in `Counter`):
#     - __add__(self, other, /)                        : Adds counts from two Counters; results exclude counts 
#                                                      : ≤ 0.
#     - __sub__(self, other, /)                        : Subtracts counts from another Counter; results 
#                                                      : exclude counts ≤ 0.
#     - __or__(self, other, /)                         : Returns the union of two Counters (maximum counts).
#     - __and__(self, other, /)                        : Returns the intersection of two Counters (minimum 
#                                                      : counts).

# 🔄 In-Place Operators (redefined in `Counter`):
#     - __ior__(self, other, /)                        : Performs in-place union (maximum counts).
#     - __iand__(self, other, /)                       : Performs in-place intersection (minimum counts).

# 🔄 Comparison Operators (redefined in `Counter`):
#     - __eq__(self, other, /)                         : Checks equality between two Counters based on element 
#                                                      : counts.
#     - __ne__(self, other, /)                         : Checks inequality between two Counters based on 
#                                                      : element counts.
#     - __lt__(self, other, /)                         : Checks if one Counter is strictly less than another 
#                                                      : (all counts must be strictly less).
#     - __le__(self, other, /)                         : Checks if one Counter is less than or equal to 
#                                                      : another (all counts must be ≤).
#     - __gt__(self, other, /)                         : Checks if one Counter is strictly greater than 
#                                                      : another (all counts must be strictly greater).
#     - __ge__(self, other, /)                         : Checks if one Counter is greater than or equal to 
#                                                      : another (all counts must be ≥).