Unique Paths II, dynamic programming problem, O(n^2) time, O(n^2) space This is based on this leetcode question.  I get the correct answer, but I know it could be much, much cleaner.  I also know there is supposed to be an O(n) space solution, but I'm not sure how to implement cleanly.
It is a dynamic programming problem, I tried to add some helpful code comments, I know it's pretty messy so thank you for your review.
class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -&gt; int:
        #initialize memoization array
        memo = [[-1 for i in range(len(obstacleGrid[0]))] for j in range(len(obstacleGrid))]


        #add known values before any calculation (last row and last column)
        memo[len(memo)-1][len(memo[0])-1] = 1
        for i in range(len(memo[0])-2,-1,-1):
            if obstacleGrid[len(memo)-1][i] != 1:
                memo[len(memo)-1][i] = memo[len(memo)-1][i+1]
            else:
                memo[len(memo)-1][i] = 0 
        
        for j in range(len(memo)-2,-1,-1):
            if obstacleGrid[j][len(memo[0])-1] != 1:
                memo[j][len(memo[0])-1] = memo[j+1][len(memo[0])-1]
            else:
                memo[j][len(memo[0])-1] = 0
        
        if obstacleGrid[len(memo)-1][len(memo[0])-1] == 1:
            return 0 
        
        #does calculations
        def helper(row,col):
            nonlocal memo
            if obstacleGrid[row][col] == 1:
                return 0 
            if memo[row][col] == -1:
                memo[row][col] = helper(row+1,col) + helper(row,col+1)
            return memo[row][col]
        
        return helper(0,0)
Nice solution, few suggestions:

Duplicated code: the functions len(memo) and len(memo[0]) are called several times. When working with a matrix is common to call m the number of rows (len(memo)) and n the number of columns (len(memo[0])). This can help to reduce the duplicated code. As @Manuel mentioned in the comments, m and n are also defined in the problem description.

Last element of a list: instead of memo[len(memo)-1] you can use memo[-1].

Input validation: this input validation:
if obstacleGrid[len(memo)-1][len(memo[0])-1] == 1:
    return 0 

is done too late in the code, after the memo matrix is fully built. Better to move it up at the beginning of the function. BTW, with the previous suggestion, it can be shortened to:
if obstacleGrid[-1][-1] == 1:
    return 0


Naming: the row is called row in the helper function and j in the rest of the code. Same for the column. Use the same name to be consistent.

Throwaway variables: in the initialization of memo:
memo = [[-1 for i in range(len(obstacleGrid[0]))] for j in range(len(obstacleGrid))]

the variables i and j are not used. They can be replaced with _.

Type hints: the helper function is missing the type hints.

LRU cache: there is a handy annotation that does the memoization automatically, @lru_cache. Or the unbounded @cache in Python 3.9.


An example using @lru_cache:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -&gt; int:
    m = len(obstacleGrid)
    n = len(obstacleGrid[0])

    if obstacleGrid[-1][-1] == 1:
        return 0

    @lru_cache(maxsize=None)
    def helper(row: int, col: int) -&gt; int:
        if row == m - 1 and col == n - 1:
            return 1
        if row &lt; m and col &lt; n:
            if obstacleGrid[row][col] == 1:
                return 0
            return helper(row + 1, col) + helper(row, col + 1)
        return 0

    return helper(0, 0)

For dynamic programming solutions, you can have a look at the &quot;Solution&quot; section or &quot;Discussion&quot; section using tags python and dynamic programming.