List traversal baby steps
For traversing lists using recursion, there's only 3 things that matter
- the empty list - always check if your list is empty before anything!
 
- the head of the list – the first item
 
- the tail of the list – the entire list except the first item
 
Ok, and here's the functions we'll use to write our programs
def is_empty (ls):
  return ls == []
def head (ls):
  return ls[0]
def tail (ls):
  return ls[1:]
Usage of the functions is straightforward
is_empty ([])           # => True
is_empty ([ 1, 2, 3 ])  # => False
head ([ 1, 2, 3 ])      # => 1
tail ([ 1, 2, 3 ])      # => [2,3]
Ok, so let's write our list_sum function – I think you'll agree that we ended up with a very readable program thanks to the list helpers we defined
def list_sum (ls):
  if is_empty (ls):
    return 0
  else:
    return head (ls) + list_sum (tail (ls))
print (list_sum ([ 1, 2, 3, 4, 5, 6, 7 ]))
# => 28
But you tagged this with tail recursion, so with the help of an optional parameter and a default value, we can move the recursive call into tail position. Changes in bold
def list_sum (ls, acc = 0):
  if is_empty (ls):
    return acc
  else:
    return list_sum (tail (ls), head (ls) + acc)
print (list_sum ([ 1, 2, 3, 4, 5, 6, 7 ]))
# => 28
But Python doesn't really have tail-call elimination, so you'd still have to do something else to ensure stack-safety.
Smoke higher-order functions every day
Using head and tail relieved us from the tedium (and ugliness) of [0] and [1:], but this pattern of list traversal is so common that we can relieve ourselves from even thinking at this level altogether!
Let's look back at our function, list_sum. This function traverses a list and performs addition using the built-in + function. What if instead of + we wanted to multiply all the elements using *?
def list_sum (ls):
  if is_empty (ls):
    return 0
  else:
    return head (ls) + list_sum (tail (ls))
def list_product (ls):
  if is_empty (ls):
    return 1
  else:              
    return head (ls) * list_product (tail (ls))
print (list_product ([ 1, 2, 3, 4, 5, 6, 7 ]))
# => 5040
The only thing that's different is 0 changed to 1 and + changed to *. We don't want to rewrite all of that each time we want to do something with a list. If we abstract those values using function parameters, we can capture the very essence of list traversal in this nice little program
from operator import add, mul
def list_reduce (f, acc, ls):
  if is_empty (ls):
    return acc
  else:
    return list_reduce (f, f (acc, head (ls)), tail (ls))
def list_sum (ls):
  return list_reduce (add, 0, ls)
def list_product (ls):
  return list_reduce (mul, 1, ls)
print (list_sum ([ 1, 2, 3, 4, 5, 6, 7 ]))
# => 28
print (list_product ([ 1, 2, 3, 4, 5, 6, 7 ]))
# => 5040
And higher-order functions can be built using other higher-order functions. Then before you know it, you're doing all sorts of high-level transformations and traversals!
Notice how we're not thinking about head or tail at this point. Hard-on-brain things like not [], ls[0] and ls[1:] are out-of-sight and consequently out-of-mind.
def map (f, ls):
  return list_reduce (lambda acc, x: acc + [ f (x) ], [], ls)
def square (x):
  return x * x
print (map (square, [ 1, 2, 3, 4, 5, 6, 7 ]))
# => [1, 4, 9, 16, 25, 36, 49]