0

I have this strange behavior where when I run a function that only changes class values that value is being passed instead of a default value to a constructor

Here's the constructor:

def __init__(self, key, *, name=None, id=None, oldInfo={}, newInfo={},
             priority=Priority.DEFAULT):
        self._key = key    
        self._name = name
        self._id = id
        self._oldInfo = oldInfo
        self._newInfo = newInfo
        self._priority = priority

        assert(not self._oldInfo or type(self._oldInfo) is dict)
        assert(not self._newInfo or type(self._newInfo) is dict)
        assert(type(self._priority) is Priority)

        self._appendix = None

Then I have some basic test cases. Here's a simple sequence that produces the issue (note: the Z's are there to force these test cases after all others)

class ZZZZBeforeCombineTestSuite(unittest.TestCase):

    def testDefaultsBroken(self):
        self.assertEqual(Info('key').string(), "key: for 'None'")

class ZZZZCombineTestSuite(unittest.TestCase):

    def testCombine(self):
        infoA = Info('A', oldInfo={'v1': 1})
        infoB = Info('B', newInfo={'v2': 2})
        infoA.combine(infoB)
        # self.assertEqual(infoA._oldInfo, {'v1': 1})
        # self.assertEqual(infoA._newInfo, {'v2': 2})

class ZZZZPostCombineTestSuite(unittest.TestCase):

    def testDefaultsBroken(self):
        self.assertEqual(Info('key').string(), "key: added '2' under 'v2' for 'None'")

    def testDefaultsFixed(self):
        self.assertEqual(Info('key', newInfo={}).string(), "key: for 'None'")

The combine method is defined as:

def combine(self, other):
    for key, value in other._oldInfo.items():
        self._oldInfo[key] = value
    for key, value in other._newInfo.items():
        self._newInfo[key] = value

All these test cases pass, which means:

  • first only _key is being set to 'key' and produces the string "key: for 'None'"
  • then calling combine using two separate classes, in a separate TestCase
  • in a third class, Info::init has {'v2': 2} as the arg for newInfo (verified in debug)
  • but when an empty dict is explicitly passed to the constructor it's fine

Why would (and how could) Info::init be getting newInfo={'v2': 2} when that value is never being set on the instance and is only being set on a completely different instance <note: the behavior goes away if infoA.combine(infoB) is removed; it only happens after that function call, which loops over values in two unrelated classes>

Is it at least safe to say this an artifact of the unittest module and won't effect the code when running elsewhere?

  • 1
    No, that's not safe to say. https://stackoverflow.com/q/1132941/3001761 – jonrsharpe Oct 11 '21 at 23:13
  • 1
    Does this answer your question? ["Least Astonishment" and the Mutable Default Argument](https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument) – Woodford Oct 11 '21 at 23:14
  • Thanks both! Along with the above link(s) I found this [Stack Overflow answer](https://stackoverflow.com/questions/9158294/good-uses-for-mutable-function-argument-default-values) and this [article](https://web.archive.org/web/20200221224620/http://effbot.org/zone/default-values.htm) helpful. It makes more sense now, though I still find the behavior strange :) – StephenMoody Oct 12 '21 at 18:31

0 Answers0