Комментарий Кевина хорош и заслуживает некоторого повторения - использование exec опасно, почти всегда есть лучший подход.
Однако, в ответ на ваш вопрос в первом случае, f1 () и f2 () находятся в глобальном пространстве имен, поэтому при вызове f2 () он может найти f1 (). Во втором случае они создаются в локальном пространстве функции foo (). Когда вызывается f2 (), он не может найти локальное определение f1 ().
Вы можете исправить это, используя:
code = """
global f1
def f1(): print "bar"
def f2(): f1()
"""
def foo():
exec(code)
f2()
foo()
Опять же - это почти наверняка не тот способ, которым вы хотите решить эту проблему.
** РЕДАКТИРОВАТЬ ** Публикуется неверная версия кода, который я проверял, эта версия - то, что я хотел включить.