Let's go on a journey.
The answer is that I started with a hunch. You're treating x as a pointer sometimes, and a value other times. That seems strange, and unlike the python. In python the thing is always access the same way, it isn't a ptr type sometimes and a value type others.
So first let's talk about scopes. In python, you aren't introducing a closure. If we do introduce a closure, like with an IIFE:
def mystery_closure():
x = 1
closure = (lambda v: lambda: v)(x)
x = 2
return x * closure()
suddenly we get 2. The IIFE/outer closure here is equivalent to the capture happening in rust. So this is
more equivalent to the rust examples than your python example. Closures are what matter, not variable mutability.
Cool, so now let's add another wrinkle: `i32` in rust isn't a mutable type, there are no mutating methods on an i32. What happens if we use a type that has mutating methods, like a vec?
Let's start in python, since python doesn't allow multiline lambdas, we have to swap to using an inner function, which is fine, this makes the structure a bit clearer in python.
def mystery_ mutable():
x = [1]
def closure():
def inner(v):
v.append(2)
return v
return inner(x)
x.append(3)
return x + closure()
And what if we do the same in rust? Well, we have to mark x as a mutable ref:
fn mystery_b() -> Vec {
let mut x = vec![1];
let ptr = &mut x as *mut Vec;
let capture = || unsafe{ (*ptr).push(2);
ptr };
x.push(3);
unsafe { x.extend(capture().as_ref().unwrap().iter()); }
return x
}
So the python value is a mutable ref, right? Well no, we're back to the whole issue of the closure being able to modify things outside itself in rust with a mut ref that we can't do with python:
def mystery_mutable():
x = [1]
def closure():
def inner(v):
v = [5]
v.append(2)
return v
return inner(x)
x.append(3)
return x + closure()
This returns [1,3,5,2] in python. If you translate it to rust with a mutable ref pattern, you'll get [5,2,5,2] and the 3 will just disappear:
fn mystery_mutable() -> Vec {
let mut x = vec![1];
let ptr = &mut x as *mut Vec;
let capture = || unsafe{ (*ptr) = vec![5,2];
ptr };
x.push(3);
unsafe { x.extend(capture().as_ref().unwrap().iter()); }
return x
}
So in python, the thing isn't a const ref, but it's not a mutable ref, either, and it's certainly not a value type.
In languages like rust and cpp we describe calls as pass by reference or pass by value. Pass by value is mostly irrelevant here. When passing by reference, you can use a mutable or immutable reference. Immutable references don't allow you to modify the object, just read it. Mutable references allow you to modify or replace the object. With normal pointers and references, if you're able to modify the referenced object you can also replace it with an entirely new object.
The reasons for this are tricky, but have to do with self references in methods (self/this has to be mutable for a mutable method to work). In rust and cpp the self reference is exposed, so you can make it point elsewhere. In python you can't do this. This means that its tricky to pass an immutable reference to a mutable object in rust/cpp, but in python this is the only way things get passed around.
Rust calls this "interior mutability", and RefCell is the way to do interior mutability with references, as opposed to copyable types. The docs for RefCell actually call out passing &self to a method that requires mutability[1] as a use for RefCell, so in general you could use the RefCell to implement a python-like set of containers that can be passed "immutably" and still modified internally. In Pseudo-rust:
struct PyVec {
backing_arr: RefCell>
}
impl PyVec {
fn push(&self, v: T) { // This isn't mutable?!
backing_arr.borrow_mut().push(v);
}
...
}
Which would match python's semantics very closely
[1]: https://doc.rust-lang.org/beta/std/cell/index.html#implement...