Question: I'm working on a program that keeps dying because of the OOM killer. I was hoping for some quick wins in reducing the memory usage without a major refactor. I tried adding __slots__ to the most common classes but I noticed the pickled size went up. Why is that? class Class: def __init__(self, a, b, c): self.a = a self.b = b self.c = c class ClassSlots: __slots__ = ["a", "b", "c"] def __init__(self, a, b, c): self.a = a self.b = b self.c = c cases = [ Class(1, 2, 3), ClassSlots(1, 2, 3), [Class(1, 2, 3) for _ in range(1000)], [ClassSlots(1, 2, 3) for _ in range(1000)] ] for case in cases: dump = pickle.dumps(case, protocol=5) print(len(dump))  with Python 3.10 prints 59 67 22041 25046 

Answer: So, on Python 3.11, let's define the following: class Foo: def __init__(self, a, b, c): self.a = a self.b = b self.c = c class Bar: __slots__ = ["a", "b", "c"] def __init__(self, a, b, c): self.a = a self.b = b self.c = c  Now, let's see: >>> import pickle >>> import pickletools >>> len(pickle.dumps(Foo(1,2,3))), len(pickle.dumps(Bar(1,2,3))) (57, 60)  So, there seems to be a three-byte difference (when we make the classes have the same length name... that accounts for 5 out of the 8 byte difference you were originally seeing) An important point to understand is that a "pickle" is basically a series of instructions on how to rebuild an object, these instructions are executed on a pickle virtual machine. We can use pickletools.dis to get a human readable disassembly of these instructions. Now, let's see what the disassembly shows us: >>> pickletools.dis(pickle.dumps(Foo(1,2,3))) 0: \x80 PROTO 4 2: \x95 FRAME 46 11: \x8c SHORT_BINUNICODE '__main__' 21: \x94 MEMOIZE (as 0) 22: \x8c SHORT_BINUNICODE 'Foo' 27: \x94 MEMOIZE (as 1) 28: \x93 STACK_GLOBAL 29: \x94 MEMOIZE (as 2) 30: ) EMPTY_TUPLE 31: \x81 NEWOBJ 32: \x94 MEMOIZE (as 3) 33: } EMPTY_DICT 34: \x94 MEMOIZE (as 4) 35: ( MARK 36: \x8c SHORT_BINUNICODE 'a' 39: \x94 MEMOIZE (as 5) 40: K BININT1 1 42: \x8c SHORT_BINUNICODE 'b' 45: \x94 MEMOIZE (as 6) 46: K BININT1 2 48: \x8c SHORT_BINUNICODE 'c' 51: \x94 MEMOIZE (as 7) 52: K BININT1 3 54: u SETITEMS (MARK at 35) 55: b BUILD 56: . STOP highest protocol among opcodes = 4  And: >>> pickletools.dis(pickle.dumps(Bar(1,2,3))) 0: \x80 PROTO 4 2: \x95 FRAME 49 11: \x8c SHORT_BINUNICODE '__main__' 21: \x94 MEMOIZE (as 0) 22: \x8c SHORT_BINUNICODE 'Bar' 27: \x94 MEMOIZE (as 1) 28: \x93 STACK_GLOBAL 29: \x94 MEMOIZE (as 2) 30: ) EMPTY_TUPLE 31: \x81 NEWOBJ 32: \x94 MEMOIZE (as 3) 33: N NONE 34: } EMPTY_DICT 35: \x94 MEMOIZE (as 4) 36: ( MARK 37: \x8c SHORT_BINUNICODE 'a' 40: \x94 MEMOIZE (as 5) 41: K BININT1 1 43: \x8c SHORT_BINUNICODE 'b' 46: \x94 MEMOIZE (as 6) 47: K BININT1 2 49: \x8c SHORT_BINUNICODE 'c' 52: \x94 MEMOIZE (as 7) 53: K BININT1 3 55: u SETITEMS (MARK at 36) 56: \x86 TUPLE2 57: \x94 MEMOIZE (as 8) 58: b BUILD 59: . STOP highest protocol among opcodes = 4  So, the first difference is that on opcode 33, the non-slotted class is missing a None, i.e.: 33: } EMPTY_DICT 34: \x94 MEMOIZE (as 4)  Vs: 33: N NONE 34: } EMPTY_DICT 35: \x94 MEMOIZE (as 4)  The rest of the instructions build the same dictionary, but then the slotted version also does: 56: \x86 TUPLE2 57: \x94 MEMOIZE (as 8)  Which creates a tuple (None, {<the dict>}) I am almost certain this is related to the difference between the results of __getstate__: >>> Foo(1,2,3).__getstate__() {'a': 1, 'b': 2, 'c': 3} >>> Bar(1,2,3).__getstate__() (None, {'a': 1, 'b': 2, 'c': 3})  That behavior is described in the pickle docs for object.__getstate__:  For a class that has an instance __dict__ and no __slots__, the default state is self.__dict__.  ...  For a class that has __slots__ and no instance __dict__, the default state is a tuple whose first item is None and whose second item is a dictionary mapping slot names to slot values described in the previous bullet.