Call by Object Reference¶
The Mental Model¶
When you call a function in Python and pass an argument, what exactly happens to the data? This question is fundamental to understanding how functions interact with the rest of your program.
Many programmers coming from other languages try to classify Python as either "call-by-value" or "call-by-reference." Python is neither. Python uses a mechanism called call-by-object-reference (sometimes called call-by-sharing).
The core idea: when you pass an argument to a function, the parameter name inside the function is bound to the same object as the argument outside the function. No copy is made. No pointer is passed. The function receives another name for the same object.
Think of it this way: if you and a colleague both have a sticky note labeled with different names, but both sticky notes are stuck to the same physical box, you are both looking at the same box. What happens when one of you tries to change the box depends on whether the box can be changed.
What Happens at the Call Site¶
```python def greet(name): print(f"Hello, {name}")
message = "Alice" greet(message) ```
When greet(message) executes:
- Python evaluates the argument
message, which refers to the string object"Alice". - The parameter
nameis bound to that same string object. - Inside the function,
nameandmessage(in the caller's scope) both point to the same object.
We can verify this with id():
```python def show_id(x): print(f"Inside function: id = {id(x)}")
value = [1, 2, 3] print(f"Before call: id = {id(value)}") show_id(value) ```
Output:
Before call: id = 140234567890
Inside function: id = 140234567890
The ids match. There is one object, and two names referring to it.
Comparison with C and C++¶
Understanding Python's calling convention is easier when contrasted with the two classical approaches.
Call by Value (C)¶
In C, passing a variable to a function copies the value into the parameter. Modifications inside the function affect only the local copy.
```c void increment(int x) { x = x + 1; // modifies local copy only }
int main() { int a = 5; increment(a); // a is still 5 } ```
Python is not call-by-value. No copy of the object is made when you pass an argument. The function receives a reference to the original object, not a duplicate.
Call by Reference (C++)¶
In C++, you can declare a reference parameter that becomes an alias for the caller's variable. Reassigning the parameter reassigns the caller's variable.
```cpp void increment(int &x) { x = x + 1; // modifies the caller's variable directly }
int main() { int a = 5; increment(a); // a is now 6 } ```
Python is not call-by-reference. Rebinding the parameter name inside the function (e.g., x = new_value) does not affect the caller's variable. It only changes what the local name points to.
Call by Object Reference (Python)¶
Python sits between the two. The parameter shares the same object as the argument, but reassigning the parameter does not reassign the argument:
```python def try_reassign(x): x = 99 # rebinds the local name x; does NOT affect the caller
a = 5 try_reassign(a) print(a) # 5 -- unchanged ```
However, if the shared object is mutable, the function can modify it through the shared reference:
```python def append_item(lst): lst.append(42) # mutates the shared object
my_list = [1, 2, 3] append_item(my_list) print(my_list) # [1, 2, 3, 42] -- changed! ```
| Convention | Copy made? | Reassignment affects caller? | Mutation affects caller? |
|---|---|---|---|
| Call by value (C) | Yes | No | N/A (copy) |
| Call by reference (C++) | No | Yes | Yes |
| Call by object reference (Python) | No | No | Yes (if mutable) |
Mutable vs Immutable: The Deciding Factor¶
The practical consequence of call-by-object-reference depends entirely on whether the object is mutable or immutable.
Immutable objects (int, float, str, tuple, frozenset): any operation that "changes" the value actually creates a new object and rebinds the name. The caller's binding is unaffected.
```python def add_exclamation(text): text = text + "!" # creates a new string, rebinds local name print(f"Inside: {text}")
greeting = "Hello" add_exclamation(greeting) print(f"Outside: {greeting}") ```
Output:
Inside: Hello!
Outside: Hello
Mutable objects (list, dict, set): operations that modify the object in place are visible to the caller, because both names still refer to the same object.
```python def add_key(d): d["new_key"] = "new_value" # modifies the shared dict
config = {"host": "localhost"} add_key(config) print(config) # {'host': 'localhost', 'new_key': 'new_value'} ```
Rebinding vs Mutating¶
The critical distinction is between rebinding a name and mutating an object.
- Rebinding:
x = something_newmakes the local namexpoint to a different object. The caller is unaffected. - Mutating:
x.append(item)orx["key"] = valchanges the object thatxpoints to. Since the caller's name points to the same object, the caller sees the change.
```python def rebind(lst): lst = [10, 20, 30] # rebinding -- no effect on caller print(f"Inside (rebind): {lst}")
def mutate(lst): lst.append(99) # mutating -- caller sees this print(f"Inside (mutate): {lst}")
original = [1, 2, 3]
rebind(original) print(f"After rebind: {original}") # [1, 2, 3]
mutate(original) print(f"After mutate: {original}") # [1, 2, 3, 99] ```
Output:
Inside (rebind): [10, 20, 30]
After rebind: [1, 2, 3]
Inside (mutate): [1, 2, 3, 99]
After mutate: [1, 2, 3, 99]
Summary¶
| Concept | Description |
|---|---|
| Call by object reference | Parameter is bound to the same object as the argument |
| No copy | The object itself is shared, not duplicated |
| Rebinding is local | Assigning to the parameter name does not affect the caller |
| Mutation is shared | Modifying a mutable object is visible to the caller |
| Immutable safety | Immutable objects cannot be mutated, so callers are always safe |
Exercises¶
Exercise 1. Consider the following code:
```python def mystery(a, b): a = a + b b.append(a)
x = 10 y = [1, 2, 3] mystery(x, y) print(x) print(y) ```
Predict the output. For each parameter (a and b), explain whether the function rebinds or mutates the object, and why x and y are or are not affected.
Solution to Exercise 1
Output:
text
10
[1, 2, 3, 13]
a = a + b: This is rebinding.astarts bound to the int10. The expressiona + bis not valid for int + list, so let us re-examine. Actually,a + bwould raise aTypeErrorbecause you cannot add anintand alist.
Let us correct the analysis. The expression a + b with a = 10 (int) and b = [1, 2, 3] (list) raises TypeError: unsupported operand type(s) for +: 'int' and 'list'.
Corrected version -- suppose the code were:
```python def mystery(a, b): a = a + 1 b.append(a)
x = 10 y = [1, 2, 3] mystery(x, y) print(x) # 10 print(y) # [1, 2, 3, 11] ```
a = a + 1: Rebinding.ais rebound to a new int11. Sinceintis immutable,xin the caller remains10.b.append(a): Mutation.bstill refers to the same list asy. Appending11modifies that shared list. The caller sees[1, 2, 3, 11].
The key insight: = is always rebinding (local only), while .append() is mutation (shared).
Exercise 2.
Write a function swap(a, b) that attempts to swap two variables. Explain why this cannot work in Python:
```python def swap(a, b): a, b = b, a
x = "hello" y = "world" swap(x, y) print(x, y) ```
What does this tell you about the difference between Python's calling convention and C++ call-by-reference? How would you achieve a swap effect in Python?
Solution to Exercise 2
Output:
text
hello world
The swap does not work. Inside swap, the local names a and b are rebound to each other's objects. But rebinding local names has no effect on the caller's names x and y. After the function returns, x still refers to "hello" and y still refers to "world".
In C++ with call-by-reference (void swap(int &a, int &b)), the parameters are true aliases for the caller's variables. Reassigning them reassigns the caller's variables. Python's call-by-object-reference does not provide this capability.
To achieve a swap in Python, you simply do it at the call site:
python
x, y = y, x
Alternatively, if the values are stored in a mutable container, the function can mutate the container:
```python def swap_in_list(lst, i, j): lst[i], lst[j] = lst[j], lst[i]
data = ["hello", "world"] swap_in_list(data, 0, 1) print(data) # ['world', 'hello'] ```
Exercise 3. A colleague claims: "Python is call-by-value because reassigning a parameter inside a function never affects the caller." Another colleague counters: "Python is call-by-reference because modifying a list inside a function affects the caller." Both are partially right but ultimately wrong. Explain why, using the following code as evidence:
```python def test(data): data.append(4) # line A data = [10, 20, 30] # line B data.append(40) # line C
original = [1, 2, 3] test(original) print(original) ```
Predict the output and explain what happens at each labeled line.
Solution to Exercise 3
Output:
text
[1, 2, 3, 4]
Line-by-line analysis:
- Line A (
data.append(4)):datais bound to the same list asoriginal. This mutates the shared object.originalis now[1, 2, 3, 4]. - Line B (
data = [10, 20, 30]): This rebinds the local namedatato a brand-new list.originalis unaffected and still refers to[1, 2, 3, 4]. From this point on,dataandoriginalrefer to different objects. - Line C (
data.append(40)): This mutates the new list[10, 20, 30]to become[10, 20, 30, 40]. Since this is a different object fromoriginal, the caller sees no effect.
Why neither "call-by-value" nor "call-by-reference" is correct:
- If Python were call-by-value, line A would not affect
original(a copy would have been made). But it does affectoriginal. So Python is not call-by-value. - If Python were call-by-reference, line B would reassign
originalto[10, 20, 30]. But it does not. So Python is not call-by-reference.
Python is call-by-object-reference: the parameter shares the object (so mutation is visible), but the parameter is an independent name (so rebinding is local).