Question

Python Argument Passing Explained: Pass by Reference vs Rebinding

pythonreferenceparameter-passingpass-by-reference

Question

I wrote the following Python class to test whether a variable can be passed by reference:

class PassByReference:
    def __init__(self):
        self.variable = 'Original'
        self.change(self.variable)
        print(self.variable)

    def change(self, var):
        var = 'Changed'

When I create an instance, the output is Original, not Changed.

Does this mean Python passes parameters by value? If not, how does Python actually pass arguments to functions and methods?

Also, how can I change this code to produce the effect people usually mean by "pass by reference," so that the final output becomes Changed?

I am also confused about why code like this does not affect the caller:

x = 1

but code like this can affect the caller:

x[0] = 1

Why does assignment to a parameter name behave differently from item assignment?

Short Answer

By the end of this page, you will understand how Python passes arguments, why assigning to a parameter does not change the caller's variable, and why mutating a mutable object can affect the caller. You will also learn practical ways to get "pass-by-reference-like" behavior in Python using objects, containers, and return values.

Concept

Python does not use traditional pass-by-reference in the same way as languages like C++ references. It also is not best described as simple pass-by-value in the everyday beginner sense.

The most useful beginner-friendly explanation is:

  • Python passes object references by assignment.
  • A function receives a new local name that refers to the same object as the caller's argument.
  • If you rebind that local name, the caller is unaffected.
  • If you mutate the shared object, the caller can observe the change.

In your example:

def change(self, var):
    var = 'Changed'

var starts by referring to the same string object as self.variable. But then var = 'Changed' makes var refer to a different string. It does not change self.variable.

This matters because Python code constantly passes lists, dictionaries, class instances, strings, numbers, and functions into other functions. To write correct code, you need to know whether you are:

  • changing the object itself, or
  • just changing which object a local variable points to

A key detail is that strings, integers, and tuples are immutable. They cannot be changed in place. So if you want a different string value, you must create a new string object and assign a name to it.

Lists, dictionaries, and most custom objects are mutable. They can be modified in place.

For example:

def rebind(x):
    x = [99]

def mutate(x):
    x[0] = 99

values = [1]
rebind(values)
print(values)   # [1]

mutate(values)
print(values)   # [99]

Why?

  • x = [99] gives the local name x a new object.
  • x[0] = 99 changes the existing list object that both names refer to.

That is the core idea behind the whole question.

Mental Model

Think of a variable name as a sticky note attached to an object.

When you call a function, Python does not send the sticky note itself. Instead, it gives the function a new sticky note pointing to the same object.

So inside the function:

  • If you move your sticky note to a new object, the caller's sticky note stays where it was.
  • If you edit the shared object, both sticky notes still point to that edited object.

Example:

  • Caller's note: self.variable -> 'Original'
  • Function parameter note: var -> 'Original'

Then inside the function:

var = 'Changed'

Now the function's sticky note points to a new string:

  • var -> 'Changed'
  • self.variable -> 'Original'

The original object was never changed.

Now compare that with a list:

  • Caller's note: items -> [1, 2, 3]
  • Function note: x -> [1, 2, 3]

If the function does:

x[0] = 99

it edits the shared list itself. Both notes still point to the same list, so both "see" the change.

Take Quiz

Syntax and Examples

Core idea

Rebinding a parameter

def change_value(x):
    x = 'Changed'

name = 'Original'
change_value(name)
print(name)  # Original

This does not change name, because x is only a local name.

Mutating a mutable object

def change_list(items):
    items[0] = 'Changed'

values = ['Original']
change_list(values)
print(values[0])  # Changed

This works because the list object is mutable and is changed in place.

Your class example

If your goal is to change an attribute on the object, the simplest fix is to modify self.variable directly:

class PassByReference:
    def __init__(self):
        self.variable = 'Original'
        self.change()
        print(self.variable)

    def change(self):
        self.variable = 'Changed'

PassByReference()

Output:

Changed

Another valid pattern: return the new value

class PassByReference:
    def __init__(self):
        self.variable = 'Original'
        self.variable = self.change(self.variable)
        print(self.variable)

    def change(self, var):
        return 'Changed'

PassByReference()

This is often the clearest style in Python.

Using a mutable container to simulate reference-like behavior

def change(box):
    box[0] = 'Changed'

value = ['Original']
change(value)
print(value[0])  # Changed

Here the list acts like a small mutable wrapper around the value.

Step by Step Execution

Consider this example:

def change(var):
    var = 'Changed'

name = 'Original'
change(name)
print(name)

Step 1: create name

name = 'Original'

name refers to the string 'Original'.

Step 2: call the function

change(name)

A new local variable var is created inside change. It refers to the same string object as name.

At this moment:

  • name -> 'Original'
  • var -> 'Original'

Step 3: rebind the local parameter

var = 'Changed'

Now var refers to a new string object.

After this line:

  • name -> 'Original'
  • var -> 'Changed'

Only the local name changed.

Step 4: function ends

var disappears because it is local to the function.

Step 5: print name

print(name)

Output:

Original

Now compare with mutation:

def change(items):
    items[0] = 'Changed'

values = ['Original']
change(values)
print(values[0])

Step 1

values refers to a list: ['Original']

Step 2

items refers to the same list.

Step 3

items[0] = 'Changed'

This changes the existing list object.

Now the shared list becomes:

['Changed']

Step 4

Printing values[0] shows:

Changed

Real World Use Cases

Understanding rebinding vs mutation is important in real Python programs.

1. Updating objects in classes

class User:
    def __init__(self, name):
        self.name = name

    def rename(self, new_name):
        self.name = new_name

You usually modify the object's attributes, not a copied parameter name.

2. Editing lists during data processing

def normalize(scores):
    for i in range(len(scores)):
        scores[i] = max(scores[i], 0)

This mutates the list in place.

3. Modifying dictionaries in configuration code

def set_defaults(config):
    config.setdefault('timeout', 30)
    config.setdefault('retries', 3)

A function can update a shared dictionary directly.

4. Returning new immutable values

def format_name(name):
    return name.strip().title()

Strings are immutable, so returning a new string is the normal approach.

5. API and library design

Some functions intentionally mutate input objects, while others return new ones. Good Python code makes this choice clear so callers know what to expect.

Take Quiz

Real Codebase Usage

In real projects, developers usually avoid trying to force pass-by-reference semantics. Instead, they use clearer Python patterns.

Common patterns

1. Return the new value

Best when working with immutable values such as strings, numbers, and tuples.

def apply_discount(price, percent):
    return price * (1 - percent)

price = 100
price = apply_discount(price, 0.2)

2. Mutate a passed-in object intentionally

Useful for lists, dictionaries, and custom class instances.

def add_log_entry(logs, message):
    logs.append(message)

3. Update object attributes through self

Common in classes.

class Task:
    def complete(self):
        self.done = True

4. Use guard clauses before mutation

def update_profile(profile):
    if profile is None:
        return
    profile['active'] = True

5. Avoid hidden side effects unless they are expected

If a function mutates its argument, make that obvious by naming and documentation.

def sort_in_place(items):
    items.sort()

This is clearer than unexpectedly modifying input inside a function with a neutral name.

Good practice

  • Return new values for immutable data.
  • Mutate objects only when that behavior is useful and expected.
  • In classes, change attributes on self.
  • Prefer clarity over trying to imitate another language's pass-by-reference features.

Common Mistakes

1. Thinking = changes the caller's variable

Broken example:

def change(x):
    x = 10

value = 5
change(value)
print(value)  # still 5

Why it happens:

  • x = 10 only rebinds the local name x.

How to avoid it:

  • Return the new value instead.
def change(x):
    return 10

value = 5
value = change(value)

2. Expecting immutable objects to change in place

Broken example:

def add_exclamation(text):
    text += '!'

message = 'Hello'
add_exclamation(message)
print(message)  # Hello

Why it happens:

  • Strings are immutable.

Correct approach:

def add_exclamation(text):
    return text + '!'

3. Accidentally mutating a list or dictionary

Broken example:

def add_item(items):
    items.append('new')

my_items = ['a', 'b']
add_item(my_items)
print(my_items)  # ['a', 'b', 'new']

This may be correct or may be an unwanted side effect.

How to avoid it:

  • Copy the object if you want isolation.
def add_item(items):
    items = items.copy()
    items.append('new')
    return items

4. Using mutable wrappers when a return value would be clearer

Possible but less clear:

def change(box):
    box[0] = 42

Often better:

def change(value):
    return 42

5. Forgetting that attributes and items are mutation targets

obj.name = 'New'
data['key'] = 'New'
items[0] = 'New'

These can affect shared objects because they modify the object, not just a local name.

Comparisons

ConceptWhat it doesAffects caller?Example
Rebinding a local variableMakes the local name point to a new objectNox = 'Changed'
Mutating a listChanges the existing list objectYesx[0] = 'Changed'
Mutating a dictChanges the existing dictionaryYesx['a'] = 1
Setting an attributeChanges the existing objectYesobj.name = 'New'
Returning a new valueCreates a result and lets caller choose whether to assign itOnly if caller reassignsx = func(x)

Strings vs lists

TypeMutable?Can change in place?Typical pattern
strNoNoReturn a new string
intNoNoReturn a new integer
tupleNoNoReturn a new tuple
listYesYesMutate or return a copy
dictYesYesMutate or return a copy
custom objectUsually yesUsually yesUpdate attributes

Your original code vs corrected versions

VersionResultWhy
var = 'Changed'self.variable stays Originallocal rebinding only
self.variable = 'Changed'output becomes Changedattribute on the object is updated
return 'Changed' and assign itoutput becomes Changedcaller explicitly replaces old value
Take Quiz

Cheat Sheet

Quick rules

  • Python passes arguments by object reference assignment.
  • Function parameters are local names.
  • x = something inside a function only rebinds the local name.
  • x[0] = ..., x['k'] = ..., and obj.attr = ... modify an existing object.
  • Immutable objects cannot be changed in place.
  • Mutable objects can usually be changed in place.

Common patterns

Rebinding does not affect caller

def f(x):
    x = 10

Mutation can affect caller

def f(items):
    items.append(10)

Best way to update immutable data

def f(x):
    return x + 1

Best way to update object state in a class

def change(self):
    self.variable = 'Changed'

Remember

  • Names are not objects.
  • Assignment changes a name binding.
  • Mutation changes an object.

For your example

Use either:

def change(self):
    self.variable = 'Changed'

or:

def change(self, var):
    return 'Changed'

FAQ

Is Python pass-by-reference or pass-by-value?

Neither label is perfect by itself. The clearest explanation is that Python passes object references by assignment.

Why does x = 1 not change the caller's variable?

Because it only rebinds the local parameter name x to a new object.

Why can x[0] = 1 change the caller's data?

Because it mutates the shared object that both caller and function refer to.

How do I get pass-by-reference behavior in Python?

Usually by mutating a mutable object, changing an attribute on self, or returning a new value and assigning it.

What is the Pythonic way to update a string inside a function?

Return the new string and assign it in the caller.

Should I use a list or dictionary just to simulate pass-by-reference?

Only when it truly improves the design. Most of the time, returning a value or updating object attributes is clearer.

Why does self.variable = 'Changed' work?

Because it updates the attribute on the instance object itself, which still exists after the method returns.

Take Quiz

Mini Project

Description

Build a small Python example that demonstrates the difference between rebinding and mutation. The project will show three common ways data can be updated in Python: returning a new immutable value, mutating a list, and changing an object attribute. This gives you a practical understanding of why Python does not behave like traditional pass-by-reference languages.

Goal

Create a script that clearly shows which operations affect the caller and which do not.

Requirements

  1. Create one function that tries to change a string parameter by assignment.
  2. Create one function that changes a list by mutating it.
  3. Create a class with a method that updates an instance attribute.
  4. Print values before and after each operation.
  5. Make the output clearly show the difference between rebinding and mutation.
Take Quiz

Keep learning

Related questions

View more