It might be less confusing if you think of the binding that's happening in the parameter expression of the 'def' statement. When you see 'def closureTest(maxIndex=maxIndex):' that is a statement like 'if' or 'while' which is followed by a suite of code to be parsed and bound to the function (callable object).
The 'def' statement is evaluated in the scope where it's found (conceptually at the same level of nesting/indentation). Its parameter expression declares how arguments will be mapped to names within the function's own scope. Any of those which provide a default (such as maxIndex in your examples) create a function object with the corresponding parameter name bound to whatever object was named or instantiated at the time (within the scope of) the 'def' statement.
When the function is called each of its parameters (names within its scope)is bound to any arguments supplied to the function. Any optional parameters are thus left bound to whichever arguments were evaluated as part of the 'def' statement.
In all of your examples an inner function is created during each invocation of the outer function. In your second example the parameter list is empty and the inner function is simply seeing the name through one level of nested scope. In the first example the inner function's def statement creates a default maxIndex name within the new function's namespace (thus preventing any resolution of the name with values from the surrounding scope, as you'd expect for any local variable within any function).
In the last example the value of maxIndex is modified before the inner function is (re)-defined. When you realize the the function is being (re)-defined on each outer function invocation then it shouldn't seem so tricky.
Python is a dynamic language. 'def' is a statement is being executed by the VM every time the flow of control passes through that line of your code. (Yes, the code has been byte compiled, but 'def' is compiled into VM op codes which perform code evaluation and name binding (to the function's name) at run-time).
If you define a function with a parameter list like '(myList=list())' then a list will be instantiated as the definition is executed. It will be accessible from within invocations of the functions code any time the function is called with no arguments. Any invocation with an argument will be executed with that parameter name bound to the argument supplied at invocation. (The object instantiated at def time is still referenced by the code object that was defined -- the suite indented after the def statement).
None of this will make any sense if you don't keep the distinction between parameters and arguments. Remember that parameters are part of the function's definition; they define how arguments will be mapped into the function's local namespace. Arguments are part of the invocation; they are the things being passed into any call of the function.
I hope this helps. I realize that the distinction is subtle and the terms are very frequently mis-used as though they were interchangeable (including throughout the Python documentation).