pythonrecursioninfinite-recursion

Why am I getting this RecursionError?


I'm working on making a program to read a chart of accounts and turn it into a tree of Account objects, which I will be doing stuff with later. The chart of accounts in question has several levels of sub-accounts, with the name of each account being indented three spaces more than its parent account. For example:

   Current Assets
      Cash
         1000000.00 - Cash
         1000010.00 - Bank Account #1
         ...
      Other Current Assets
         Receivables
            100800.00 - Accounts Receivable
            ...

I've created a class to represent an account, as follows:

class Account:
    parent = None
    num: int | None
    name: str  # Name after stripping whitespace and splitting off account number
    raw: str   # Name prior to ^
    ...  # Assorted irrelevant class variables
    acct_type: str | None
    detail_type: str | None
    _header: bool = False  # Flags whether an account's a header account
    descendants: list = list() # Holds account's children. Should be empty if _header == False.

    def __init__(self, instr: str, parent=None, acct_type: str = None, detail_type: str = None):
        ''' Initialize an Account.
        
        - parent: Account. `None` if top-level.
        - instr: raw string from ACS CoA.
        - acct_type: QBO Account Type
        - detail_type: QBO Detail Type
        '''
        self.raw = instr
        self.parent = parent
        self.acct_type = acct_type
        self.detail_type = detail_type
        stripped = instr.strip()
        ...  # Assorted initialization code, incl. manipulating stripped 
             # to get values for self.num and self.name
    ...  # Assorted irrelevant methods
    def is_header(self) -> bool:
        return self._header
    def depth(self) -> int:
        return (len(self.raw) - len(self.raw.strip())) / 3 # self.raw
    def add_descendant(self, instr, acct_t: str = None, detail: str = None): 
        '''Add descendant to account.

        If `acct_type` and `detail_type` are supplied, will override parent account type.
        '''
        if len(self.descendants) > 0:
            if calc_depth(instr) > self.descendants[-1].depth():
                self.descendants[-1].add_descendant(instr, acct_t = acct_t, detail = detail)
            else:
                self.descendants.append(Account(
                    instr,
                    parent=self, 
                    acct_type = self.acct_type if acct_t is None else acct_t,
                    detail_type = self.detail_type if detail is None else detail
                ))
        else:
            self.descendants.append(Account(
                instr,
                parent=self, 
                acct_type = self.acct_type if acct_t is None else acct_t,
                detail_type = self.detail_type if detail is None else detail
            ))
        return

if __name__ == "__main__":
    accounts: list[Account] = list()
    for (acs_name, account_type, detail_type) in get_data(): # Gets data as list[tuple[str, str, str]]
        d = len(acs_name) - len(acs_name.lstrip())) / 3  # Depth of current row's account
        if d == 0:  # Overall header
            accounts.append(Account(acs_name, parent=None, acct_type=account_type, detail_type=detail_type))
        else:
            if account_type is not None:
                # Account metadata supplied in CoA spreadsheet
                accounts[-1].add_descendant(
                    acs_name,
                    acct_t = account_type,
                    detail = detail_type)
            else:
                # Impute account metadata from parent
                accounts[-1].add_descendant(acs_name)

However, when I run this, I get a rather nasty RecursionError:

Exception has occurred: RecursionError
maximum recursion depth exceeded
KeyError: 'C:\\Users\\...\\Account.py'

During handling of the above exception, another exception occurred:

KeyError: 'C:\\Users\\...\\Account.py'

During handling of the above exception, another exception occurred:

KeyError: 'C:\\Users\\...\\Account.py'

During handling of the above exception, another exception occurred:

KeyError: 'C:\\Users\\...\\Account.py'

During handling of the above exception, another exception occurred:

  File "C:\Users\...\Account.py", line 104, in add_descendant
    self.descendants[-1].add_descendant(instr, acct_t = acct_t, detail = detail)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\...\Account.py", line 104, in add_descendant
    self.descendants[-1].add_descendant(instr, acct_t = acct_t, detail = detail)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\...\Account.py", line 104, in add_descendant
    self.descendants[-1].add_descendant(instr, acct_t = acct_t, detail = detail)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  [Previous line repeated 980 more times]
  File "C:\Users\...\Account.py", line 155, in <module>
            acs_name,
            ^^^^^^^^^
    ...<3 lines>...
            detail=detail_type
    
RecursionError: maximum recursion depth exceeded

I've spent the last 1.5 hours trying to fix this, but to no avail. Looking in my debugger, it appears that while the first two levels get added properly, that second level makes itself its own descendant ad infinitum. For example, in my most recent debugging run the tree looks like this:

<__main__.Account object at 0x0000015631511BE0>   # Current Assets
  <__main__.Account object at 0x0000015631796710>   # Cash
    <__main__.Account object at 0x0000015631796710>   # Cash
      <__main__.Account object at 0x0000015631796710>   # Yet more Cash
        ...  # 980-ish more Cash

For reference, it should actually look like this:

<__main__.Account object at 0x0000015631511BE0>   # Current Assets
  <__main__.Account object at 0x0000015631796710>   # Cash
    <__main__.Account object at 0x0000015631XXXXXX>   # 1000000.00 - Cash

What am I doing wrong? As far as I can tell, there isn't anywhere where I am adding self to self.descendants.

(As a final note, this isn't my full code; irrelevant bits have been omitted or simplified.)


Solution

  • In this code:

    def add_descendant(self, instr, acct_t: str = None, detail: str = None): 
        '''Add descendant to account.
    
        If `acct_type` and `detail_type` are supplied, will override parent account type.
        '''
        if len(self.descendants) > 0:
            if calc_depth(instr) > self.descendants[-1].depth():
                self.descendants[-1].add_descendant(instr, acct_t = acct_t, detail = detail)
    

    If the first two conditions are true, it calls itself recursively.

    Then in the recursive call, those same two conditions are still true (because, as @Blckknght pointed out, self.descendants is a shared class attribute, not a per-instance attribute). So it's going to call itself again.

    And again.

    And again.