Project Euler #15 in Python
The code converts a pair of numbers to a dictionary key by putting the numbers in a list and then turning the list into a string: str([a, b]). But that's not necessary: you can use the tuple (a, b) as a dictionary key without having to convert it to a string.

(The reason that you can't use a list [a, b] as a dictionary key is that lists can be modified, which invalidates their use as keys. Tuples can't be modified, and so you can use them as keys.)
It would simplify the code if the function routes (i) started by looking up its parameters in the results dictionary to see if it already had an answer; and (ii) always stored its result in the dictionary. If you did both of these, then you could avoid a lot of the cases:

results = {}

def routes(a, b):
    "Number of routes from (a, b) to (0, 0) in a grid."
    if (a, b) in results:
        return results[a, b]
    elif a &gt; b:
        r = routes(b, a)
    elif a == 0:
        r = 1
    else:
        r = routes(a - 1, b) + routes(a, b - 1)
    results[a, b] = r
    return r

This code does two things: it computes the number of routes, and it arranges to cache the results of the computation in a dictionary to avoid having to recompute it. The principle of separation of concerns suggests that we should try to split these two things into different bits of code. That would make them easier to understand, test, and reuse.

In this case, Python already has built-in support for caching the results of a function: see functools.lru_cache. So all you need to implement is the route-counting part of the code, and let lru_cache do the caching:

from functools import lru_cache

@lru_cache(maxsize=None)
def routes(a, b):
    "Number of routes from (a, b) to (0, 0) in a grid."
    if a &gt; b:
        return routes(b, a)
    elif a == 0:
        return 1
    else:
        return routes(a - 1, b) + routes(a, b - 1)