Unravelling subscriptions in Python

For the next post in my syntactic sugar series I want to cover subscriptions. It's quite possible you're not familiar with this formal term, but you are probably familiar with the syntax: the square brackets used for indexing lists and tuples (sequence[4]), accessing the value of a specified dictionary (dictionary["key"]), etc. To cover this topic we will break up into three parts: general subscriptions, slicing, and multiple arguments.

General subscriptions

A subscription can get, set, or delete items from a collection. These three operations have equivalent special methods called __getitem__, __setitem__, and __delitem__, respectively. Due to the fact that if a subscription is done on an object that does not have an appropriate special method, we will re-implement the appropriate functions from the operator module. All three functions take a similar approach, so I will just show how __getitem__ works and let you look at the source code for the other two functions.

def __getitem__(container, index, /):
    """Return the item in the container at the specified index."""
    container_type = type(container)
    try:
        getitem_method = debuiltins._mro_getattr(container_type, "__getitem__")
    except AttributeError:
        raise TypeError(f"{container_type.__name__!r} object is not subscriptable")
    return getitem_method(container, index)
Implementation of operator.__getitem__

The code:

  1. Gets the type of the container.
  2. Gets the __getitem__ method from the type.
  3. If the method doesn't exist, raise TypeError.
  4. Otherwise call the method appropriately.

Slicing

The syntax for slicing maps to the slice class' constructor where any empty value is represented by None.

  • :: maps to slice(None, None, None)
  • 1:2:3 maps to slice(1, 2, 3)
  • 1:2 maps to slice(1, 2)
  • : maps to slice(None, None)
  • 1: maps to slice(1, None)
  • :1 maps to slice(None, 1) (maps to slice(1) as well)

The slice object then gets passed into the appropriate special method, so x[1:2:3] is the equivalent of type(x).__getitem__(x, slice(1, 2, 3)).

Multiple arguments

If you don't work with the scientific stack and use packages like NumPy, you may not know that you can actually pass in multiple arguments when using the subscription syntax: [1, 2, 3]. The key difference to a function call, though, is all of the values get bundled up into a tuple that gets passed in as the first argument to the appropriate special method. This translates x[1, 2, 3] to type(x).__getitem__((1, 2, 3)). This also means that passing in a tuple with the same values is no different: x[1, 2, 3] == x[(1, 2, 3)].