pythonsetpicklesubclass

Pickling set subclass raises unhashable type: 'list'


This code (using custom list subclass) works well for me:

import pickle

class Numbers(list):
    def __init__(self, *numbers: int) -> None:
        super().__init__()
        self.extend(numbers)

numbers = Numbers(12, 34, 56)
numbers.append(78)

numbers = pickle.loads(pickle.dumps(numbers, protocol=3))

but when I change the parent class to set:

import pickle

class Numbers(set):
    def __init__(self, *numbers: int) -> None:
        super().__init__()
        self.update(numbers)

numbers = Numbers(12, 34, 56)
numbers.add(78)

numbers = pickle.loads(pickle.dumps(numbers, protocol=3))

the code raises a TypeError with this traceback:

Traceback (most recent call last):
  File "test.py", line 11, in <module>
    numbers = pickle.loads(pickle.dumps(numbers, protocol=3))
  File "test.py", line 6, in __init__
    self.update(numbers)
TypeError: unhashable type: 'list'

The set subclass is successfully initialized and works well but trying to pickle it raises a very confusing exception since there is actually no list used in my code.


Solution

  • Summary

    Modifying the constructor of a built-in type is hard and error-prone since other methods migth depend on it. Avoid whenever possible.

    Error inspection

    First, by forcing the Python implementation over the compiled C implementation of the pickle module we get more information in the traceback:

    Traceback (most recent call last):
      File "test.py", line 14, in <module>
        numbers = pickle.loads(pickle.dumps(numbers, protocol=3))
      File "/usr/local/lib/python3.7/pickle.py", line 1604, in _loads
        encoding=encoding, errors=errors).load()
      File "/usr/local/lib/python3.7/pickle.py", line 1086, in load
        dispatch[key[0]](self)
      File "/usr/local/lib/python3.7/pickle.py", line 1437, in load_reduce
        stack[-1] = func(*args)
      File "test.py", line 6, in __init__
        self.update(numbers)
    TypeError: unhashable type: 'list'
    

    Although the documentation states:

    When a class instance is unpickled, its __init__ method is usually not invoked.

    we can see from the traceback above that the __init__ method is invoked if the pickled representation of the class contains the REDUCE opcode (generally when the class implements a custom __reduce__ method) and, as the inspection of the pickled representation shows, the REDUCE opcode is indeed present:

     0: \x80 PROTO      3
     2: c    GLOBAL     '__main__ Numbers'
    20: q    BINPUT     0
    22: ]    EMPTY_LIST
    23: q    BINPUT     1
    25: (    MARK
    26: K        BININT1    56
    28: K        BININT1    34
    30: K        BININT1    12
    32: K        BININT1    78
    34: e        APPENDS    (MARK at 25)
    35: \x85 TUPLE1
    36: q    BINPUT     2
    38: R    REDUCE
    39: q    BINPUT     3
    41: }    EMPTY_DICT
    42: q    BINPUT     4
    44: b    BUILD
    45: .    STOP
    

    Solution

    Avoid modifying the constructor:

    import pickle
    
    class Numbers(set):
        pass
    
    numbers = Numbers([12, 34, 56])
    numbers.add(78)
    
    numbers = pickle.loads(pickle.dumps(numbers, protocol=3))
    

    or, if you really have to, at least make sure to pass all the arguments to the parent constructor:

    import pickle
    
    class Numbers(set):
        def __init__(self, *args, **kwargs) -> None:
            super().__init__(*args, **kwargs)
    
    numbers = Numbers([12, 34, 56])
    numbers.add(78)
    
    numbers = pickle.loads(pickle.dumps(numbers, protocol=3))