Skip to content

Instantly share code, notes, and snippets.

@asoplata
Last active May 11, 2026 18:48
Show Gist options
  • Select an option

  • Save asoplata/bfbc19fa345c1c77566e81275ef9ee65 to your computer and use it in GitHub Desktop.

Select an option

Save asoplata/bfbc19fa345c1c77566e81275ef9ee65 to your computer and use it in GitHub Desktop.
Python usage: mutable vs immutable types inside functions

Below is a python script you can copy and run that will illustrate how, inside a function that takes an argument, how that argument behaves depends both on type it is and whether it is rebound or not. This is intended to provide explicit examples of how Python is neither "pass-by-value" nor "pass-by-reference", but instead something more complicated, which is commonly called "pass-by-object-reference".

# This is extended from asking claude about if python is pass-by-val or -by-ref, the answer being "neither, it's complicated and something else called pass-by-obj-ref"
#
# # Immutable types: int, float, str, tuple
# - These basically work like "pass-by-value" where an entirely new obj is created inside the func, independent of the outer scope:
def immu_pbv_int(x):
    x = x + 1  # creates a new int object, rebinds local x
    print(f"INSIDE immu_pbv_int: {x} ")


def immu_pbv_float(x):
    x = x + 1.0  # creates a new float object, rebinds local x
    print(f"INSIDE immu_pbv_float: {x} ")


def immu_pbv_str(x):
    x = str(x) + "asdf"  # creates a new str object, rebinds local x
    print(f"INSIDE immu_pbv_str: {x} ")


def immu_pbv_tuple(x):
    # Note: tuples only have methods ".count" and ".index", and even though the below
    # works, this is probably not considered to be "changing" an existing tuple and
    # instead is probably considered to be creating a "new" one and then rebinding
    # it...even though in practice it changes the overall data in the tuple

    x = x + (4,)  # creates a new tuple object, rebinds local x
    print(f"INSIDE immu_pbv_tuple: {x} ")


# # Mutable types: list, dict, set
# - In contrast, modifying these mutable types inside a function (UNLESS YOU REBIND) will change the original, similar to pass-by-ref


def mu_pbf_list_norebind(x):
    x.append(4)
    print(f"INSIDE mu_pbf_list_norebind: {x}")


def mu_pbf_dict_norebind(x):
    x["asdf"] = 4
    print(f"INSIDE mu_pbf_dict_norebind: {x}")


def mu_pbf_set_norebind(x):
    x.add(4)
    print(f"INSIDE mu_pbf_set_norebind: {x}")


# - For mutable types though, if you DO rebind the name inside the function, then they act like "pass-by-value" in that you create a new obj and rebind the name
def mu_pbf_list_rebind(x):
    x = [1, 2, 3]
    x.append(4)
    print(f"INSIDE mu_pbf_list_rebind: {x}")


def mu_pbf_dict_rebind(x):
    x = {"omg": 1}
    x["asdf"] = 4
    print(f"INSIDE mu_pbf_dict_rebind: {x}")


def mu_pbf_set_rebind(x):
    x = {1, 2, 3}
    x.add(4)
    print(f"INSIDE mu_pbf_set_rebind: {x}")


if __name__ == "__main__":
    print("------------------------------------------------")
    print("=== IMMUTABLE TYPES (int, float, str, tuple) ===")
    print("------------------------------------------------")
    print(
        "Sort of like pass-by-value, where a new copy is made inside that is independent of the parent scope"
    )

    a = 3
    print(f"\nInput: {a}")
    immu_pbv_int(a)
    print(f"After and OUTSIDE immu_pbv_int: {a} ")

    a = 3.2
    print(f"\nInput: {a}")
    immu_pbv_float(a)
    print(f"After and OUTSIDE immu_pbv_float: {a} ")

    a = "omg"
    print(f"\nInput: {a}")
    immu_pbv_str(a)
    print(f"After and OUTSIDE immu_pbv_str: {a} ")

    a = (1, 2, 3)
    print(f"\nInput: {a}")
    immu_pbv_tuple(a)
    print(f"After and OUTSIDE immu_pbv_tuple: {a} ")

    print("\n------------------------------------------------------")
    print("=== MUTABLE TYPES (list, dict, set) with NO rebind ===")
    print("------------------------------------------------------")
    print(
        "Sort of like pass-by-ref where the variable in the outer scope IS updated. In other words, a new inner value is NOT created"
    )
    a = [1, 2, 3]
    print(f"\nInput: {a}")
    mu_pbf_list_norebind(a)
    print(f"After and OUTSIDE mu_pbf_list_norebind: {a} ")

    a = {"omg": 1}
    print(f"\nInput: {a}")
    mu_pbf_dict_norebind(a)
    print(f"After and OUTSIDE mu_pbf_dict_norebind: {a} ")

    a = {1, 2, 3}
    print(f"\nInput: {a}")
    mu_pbf_set_norebind(a)
    print(f"After and OUTSIDE mu_pbf_set_norebind: {a} ")

    print("\n---------------------------------------------------")
    print("=== MUTABLE TYPES (list, dict, set) with REBIND ===")
    print("---------------------------------------------------")
    print(
        " Again sort of like pass-by-value where a brand new inner var is created that is independent of the outer scope"
    )
    a = [1, 2, 3]
    print(f"\nInput: {a}")
    mu_pbf_list_rebind(a)
    print(f"After and OUTSIDE mu_pbf_list_rebind: {a} ")

    a = {"omg": 1}
    print(f"\nInput: {a}")
    mu_pbf_dict_rebind(a)
    print(f"After and OUTSIDE mu_pbf_dict_rebind: {a} ")

    a = {1, 2, 3}
    print(f"\nInput: {a}")
    mu_pbf_set_rebind(a)
    print(f"After and OUTSIDE mu_pbf_set_rebind: {a} ")

Output:

------------------------------------------------
=== IMMUTABLE TYPES (int, float, str, tuple) ===
------------------------------------------------
Sort of like pass-by-value, where a new copy is made inside that is independent of the parent scope

Input: 3
INSIDE immu_pbv_int: 4
After and OUTSIDE immu_pbv_int: 3

Input: 3.2
INSIDE immu_pbv_float: 4.2
After and OUTSIDE immu_pbv_float: 3.2

Input: omg
INSIDE immu_pbv_str: omgasdf
After and OUTSIDE immu_pbv_str: omg

Input: (1, 2, 3)
INSIDE immu_pbv_tuple: (1, 2, 3, 4)
After and OUTSIDE immu_pbv_tuple: (1, 2, 3)

------------------------------------------------------
=== MUTABLE TYPES (list, dict, set) with NO rebind ===
------------------------------------------------------
Sort of like pass-by-ref where the variable in the outer scope IS updated. In other words, a new inner value is NOT created

Input: [1, 2, 3]
INSIDE mu_pbf_list_norebind: [1, 2, 3, 4]
After and OUTSIDE mu_pbf_list_norebind: [1, 2, 3, 4]

Input: {'omg': 1}
INSIDE mu_pbf_dict_norebind: {'omg': 1, 'asdf': 4}
After and OUTSIDE mu_pbf_dict_norebind: {'omg': 1, 'asdf': 4}

Input: {1, 2, 3}
INSIDE mu_pbf_set_norebind: {1, 2, 3, 4}
After and OUTSIDE mu_pbf_set_norebind: {1, 2, 3, 4}

---------------------------------------------------
=== MUTABLE TYPES (list, dict, set) with REBIND ===
---------------------------------------------------
 Again sort of like pass-by-value where a brand new inner var is created that is independent of the outer scope

Input: [1, 2, 3]
INSIDE mu_pbf_list_rebind: [1, 2, 3, 4]
After and OUTSIDE mu_pbf_list_rebind: [1, 2, 3]

Input: {'omg': 1}
INSIDE mu_pbf_dict_rebind: {'omg': 1, 'asdf': 4}
After and OUTSIDE mu_pbf_dict_rebind: {'omg': 1}

Input: {1, 2, 3}
INSIDE mu_pbf_set_rebind: {1, 2, 3, 4}
After and OUTSIDE mu_pbf_set_rebind: {1, 2, 3}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment