It is to do with memory allocation and how exec runs.
First of all, variables in a function are stored in a stack, the memory for all variables in the function is allocated when the bytecode is compiled (note this does not mean that they are given a value at that time). If you define the variable "c" in your function somewhere, it will be allocated a space in memory. If you want to learn more about this from someone who explained it a lot better than I did, I suggest you check out this post: How is memory allocated for variables in Python?
Secondly, exec() is run dynamically and cannot edit any local variables (check out the documentation: https://docs.python.org/3/library/functions.html#exec). Dynamically means that the statement in the brackets is not compiled to bytecode with the rest of the function but when the exec bytecode is run. The exec function can create local variables but not edit them directly, this includes variables yet to be declared in the scope, as they will have memory allocated to them (even if there is no value stored there yet).
Finally, in Code1, the variable "c" is not defined outside the exec(), so when the exec is run "c" is then allocated the memory and is assigned the value. However, in Code2, "c" is defined in the function (even if it is after the exec()), so the memory is allocated when the function is compiled to bytecode, meaning that when the exec function runs, it won't be able to change the value of "c" as it already exists in memory.