It turns out that this is not actually as surprising as I had first thought -- del unbinds a variable from the (local) scope. Since exec results in a new Python frame, it is in that frame's scope that x is deleted, not in the outer frame (of the function exec_code_object_and_return_x).
I'm in the process of blogging about this and other quirks of exec -- I'll post back here once that's up.