Early in my programming career, I learned how to extract elements from a list using Python's slicing operator ([]):

my_list = list(range(10))
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

print(my_list[3:5])
# [3, 4]

print(my_list[3:])
# [3, 4, 5, 6, 7, 8, 9]

print(my_list[:5])
# [0, 1, 2, 3, 4]

But it wasn't until this week that I thought more critically about how this operator works and about how unusual it is.

For one thing, the notation is strange. That colon delimiter used inside the brackets is quite different from the usual way in which Python functions separate their arguments, i.e. using commas. That's because the comma already has a function inside brackets; it's used to separate elements that are in a list!

Second, I hadn't understood how the slicing operator worked under-the-hood. The arguments are first converted to a slice object, which accepts up to 3 arguments when being initialized. Three arguments?! Yes, Python lists can be sliced from any starting point up to any stopping point using any step size of your choosing (default: 1). Here it is in action:

print(my_list[3::2])
# [3, 5, 7, 9]

print(my_list[slice(3, None, 2)])
# [3, 5, 7, 9]

print(my_list[3::-1])
# [3, 2, 1, 0]

print(my_list[slice(3, None, -1)])
# [3, 2, 1, 0]

Afterwards, I remembered that I often use the -1 step size to iterate elements of a list in reverse, so I'm not sure why I was so surprised to discover that slice objects can accept 3 arguments. I guess it's easy to forget about the total number of arguments when using the colon notation, but also, I had never taken the time to think more critically about the slicing syntax. I'd learned about it so early that I'd taken it for granted. Not anymore!