An identifier may be bound to an object in Python, or it may not be bound to anything. That's all.
If identifier is bound to an object, del identifier removes the binding, leaving identifier not bound to anything. And that's all. It has no direct effect on whether memory is released.
When the last binding to an object is removed, the object becomes eligible for garbage collection. Not before then.
In the code you showed, it's actually not possible to determine when the object initially bound to res becomes eligible for garbage collection. For all we know, e.g., in
res = self.dict_cursor.fetchall()
the fetchall() returned an object from an internal persistent cache. Then
del res
would remove the binding to the object via res, but would have no effect on any other possible bindings to the object. The object can't be reclaimed until all bindings are gone.
In any case, the del res in:
del res res = anything
serves no real purpose. res ends up being bound to anything regardless of whether del res is present, and so the original binding of res is removed too regardless of whether del res is present.
deepcopy(), because native types/structures aren't mutable in the first place (and creating a new list/dict with a change doesn't change anything but the parents of the point in the tree where the change was made, reducing the amount of content that actually needs to be duplicated).