There is nothing different about the behaviour of functions. What is different is that in one of them you rebound the name:
input_string = 'NEW'
This sets the name input_string to a new object. In the other function you make no assignments to a name. You only call a method on the object, and assign to indices on the object. This happens to alter the object contents:
input_list.append(len(input_list))
input_list[0] = 11
Note that assigning to an index is not the same as assigning to a name. You could assign the list object to another name first, then do the index assignment separately, and nothing would change:
_temp = input_list
_temp[0] = 11
because assigning to an index alters one element contained in the list, not the name that you used to reference the list.
Had you assigned directly to input_list, you'd have seen the same behaviour:
input_list = []
input_list.append(len(input_list))
input_list[0] = 11
You can do this outside a function too:
>>> a_str = 'OLD'
>>> b_str = a_str
>>> b_str = 'NEW'
>>> a_list = ['foo', 'bar', 'baz']
>>> b_list = a_list
>>> b_list.append('NEW')
>>> b_list[0] = 11
>>> a_str
'OLD'
>>> b_str
'NEW'
>>> a_list
[11, 'bar', 'baz', 'NEW']
>>> b_list
[11, 'bar', 'baz', 'NEW']
The initial assignments to b_str and b_list is exactly what happens when you call a function; the arguments of the function are assigned the values you passed to the function. Assignments do not create a copy, they create additional references to the object.
If you wanted to pass in a copy of the list object, do so by creating a copy:
new_list = old_list[:] # slicing from start to end creates a shallow copy