which happens in Julia, too:
julia> function f1()
x = 1
g() = (x = 2; x)
@show g()
@show x
end;
julia> f1();
g() = 2
x = 2
julia> g() = (x = 2; x);
julia> function f2()
x = 1
@show g()
@show x
end;
julia> f2();
g() = 2
x = 1
Note that the requirement of Python’s nonlocal, Rust’s mut and C++'s & can prevent this type of “bug.”
I wouldn’t argue that Python’s design of inner function is the best approach, but I’d stress that it has nice properties (which could just be a coincidence) that is worth analyzing, especially when it comes to concurrent and parallel programming. I believe there are something beyond beginner friendliness like you concluded.
I also don’t think it’s a good idea to look at Scheme etc. and conclude that environment-mutation is the defining property of inner functions. For example, it’s pretty obvious that Clojure emphasize closures but it also focuses on immutability. You have to use something like Julia’s Ref to communicate back the mutation in the closures to the outer function. I can understand the perspective that it works only because Clojure is a functional language that focuses on immutability, as you mentioned in the comment above. But Rust also shows that you can learn lessons from purely functional languages even if you are designing an imperative language (if it emphasizes concurrency and parallelism).