How to re-assign a variable in python without changing its id?

I'm not sure whether you're confused about variables in Python, or about immutable values. So I'm going to explain both, and half the answer will probably seem like "no duh, I already knew that", but the other half should be useful.


In Python—unlike, say, C—a variable is not a location where values live. It's just a name. The values live wherever they want to.1 So, when you do this:

a = 10
b = a

You're not making b into a reference to a. That idea doesn't even make sense in Python. You're making a into a name for 10, and then making b into another name for 10. And if you later do this:

a = 11

… you've made a into a name for 11, but this has no effect on b—it's still just a name for 10.


This also means that id(a) is not giving you the ID of the variable a, because there is no such thing. a is just a name that gets looked up in some namespace (e.g., a module's globals dict). It's the value, 11 (or, if you ran it earlier, the different value 10) that has an ID. (While we're at it: it's also values, not variables, that are typed. Not relevant here, but worth knowing.)


Things get a bit tricky when it comes to mutability. For example:

a = [1, 2, 3]
b = a

This still makes a and b both names for a list.

a[0] = 0

This doesn't assign to a, so a and b are still names for the same list. It does assign to a[0], which is part of that list. So, the list that a and b both name now holds [0, 2, 3].

a.extend([4, 5])

This obviously does the same thing: a and b now name the list [0, 2, 3, 4, 5].


Here's where things get confusing:

a += [6]

Is it an assignment that rebinds a, or is it just mutating the value that a is a name for? In fact, it's both. What this means, under the covers, is:

a = a.__iadd__([6])

… or, roughly:

_tmp = a
_tmp.extend([6])
a = _tmp

So, we are assigning to a, but we're assigning the same value back to it that it already named. And meanwhile, we're also mutating that value, which is still the value that b names.


So now:

a = 10
b = 10
a += 1

You probably can guess that the last line does something like this:

a = a.__iadd__(1)

That's not quite true, because a doesn't define an __iadd__ method, so it falls back to this:

a = a.__add__(1)

But that's not the important bit.2 The important bit is that, because integers, unlike lists, are immutable. You can't turn the number 10 into the number 11 the way you could in INTERCAL or (sort of) Fortran or that weird dream you had where you were the weirdest X-Man. And there's no "variable holding the number 10" that you can set to 11, because this isn't C++. So, this has to return a new value, the value 11.

So, a becomes a name for that new 11. Meanwhile, b is still a name for 10. It's just like the first example.


But, after all this telling you how impossible it is to do what you want, I'm going tell you how easy it is to do what you want.

Remember earlier, when I mentioned that you can mutate a list, and all the names for that list will see the new value? So, what if you did this:

a = [10]
b = a
a[0] += 1

Now b[0] is going to be 11.


Or you can create a class:

class Num:
    pass

a = Num()
a.num = 10
b = a
a.num += 1

Now, b.num is 11.


Or you can even create a class that implements __add__ and __iadd__ and all the other numeric methods, so it can hold numbers (almost) transparently, but do so mutably.

class Num:
    def __init__(self, num):
        self.num = num
    def __repr__(self):
        return f'{type(self).__name__}({self.num})'
    def __str__(self):
        return str(self.num)
    def __add__(self, other):
        return type(self)(self.num + other)
    def __radd__(self, other):
        return type(self)(other + self.num)
    def __iadd__(self, other):
        self.num += other
        return self
    # etc.

And now:

a = Num(10)
b = a
a += 1

And b is a name for the same Num(11) as a.

If you really want to do this, though, you should consider making something specific like Integer rather than a generic Num that holds anything that acts like a number, and using the appropriate ABC in the numbers module to verify that you covered all the key methods, to get free implementations for lots of optional methods, and to be able to pass isinstance type checks. (And probably call num.__int__ in its constructor the way int does, or at least special-case isinstance(num, Integer) so you don't end up with a reference to a reference to a reference… unless that's what you want.)


1. Well, they live wherever the interpreter wants them to live, like Romanians under Ceaușescu. But if you're a builtin/extension type written in C and a paid-up member of the Party, you could override __new__ with a constructor that doesn't rely on super to allocate, but otherwise you have no choice.

2. But it's not completely unimportant. By convention (and of course in all builtin and stdlib types follow the convention), __add__ doesn't mutate, __iadd__ does. So, mutable types like list define both, meaning they get in-place behavior for a += b but copying behavior for a + b, while immutable types like tuple and int define only __add__, so they get copying behavior for both. Python doesn't force you to do things this way, but your type would be very strange if it didn't pick one of those two. If you're familiar with C++, it's the same—you usually implement operator+= by mutating in-place and returning a reference to this, and operator+ by copying and then returning += on the copy, but the language doesn't force you to, it's just confusing if you don't.