In Python, every piece of data is an object, from simple integers and strings to complex functions and modules. This fundamental principle is often summarized as “everything is an object.” Understanding this model is crucial for mastering variable assignment, mutability, and memory management. A variable in Python is not a container that holds an object, like a box holding a value. Instead, it is a name, or a reference, bound to an object, like a label or tag attached to the object itself. This distinction is the cornerstone of Python’s object model.

The Nature of Names and References

When you write x = 10, you are not creating a variable x and putting the value 10 inside it. You are creating an integer object 10 (if it doesn’t already exist) and then binding the name x to that object in the current namespace. The assignment operator (=) is best read as “binds the name on the left to the object on the right.” Multiple names can be bound to the same object, creating multiple references to it.

a = 1000  # Name 'a' is bound to the integer object 1000
b = a     # Name 'b' is now also bound to the same object as 'a'
print(a is b)  # Output: True (they are the same object in memory)

Identity, Type, and Value

Every object in Python has three core attributes: an identity, a type, and a value.

  • Identity: This is the object’s unique, constant identifier, essentially its memory address. It can be checked with the id() function. The is operator compares the identity of two objects.
  • Type: This defines the operations the object supports (e.g., what methods it has) and is accessible via the type() function. The type is fixed for the lifetime of the object.
  • Value: The actual data contained within the object.
my_list = [1, 2, 3]
print(f"ID: {id(my_list)}")        # Output: ID: 140234567890000 (example address)
print(f"Type: {type(my_list)}")    # Output: Type: <class 'list'>
print(f"Value: {my_list}")         # Output: Value: [1, 2, 3]

Mutable vs. Immutable Objects

This is a critical distinction governed by the object model. An object’s mutability defines whether its value can be changed after creation without changing its identity.

  • Immutable Objects: Their value cannot be altered in-place. Types like int, float, str, tuple, frozenset, and bytes are immutable. Operations that seem to change them actually create a new object.
  • Mutable Objects: Their value can be changed in-place. Their identity remains the same even as their content changes. Types like list, dict, set, and most custom classes are mutable.

This difference has profound implications for assignment and function arguments.

# Immutable example (int)
x = 5
print(id(x))  # Output: e.g., 140735883043520
x = x + 1     # Creates a new integer object '6', binds 'x' to it.
print(id(x))  # Output: a different address, e.g., 140735883043552

# Mutable example (list)
my_list = [1, 2]
print(id(my_list)) # Output: e.g., 140234561234000
my_list.append(3)  # Modifies the list object in-place.
print(my_list)     # Output: [1, 2, 3]
print(id(my_list)) # Output: same address, 140234561234000

Assignment vs. Modification

Because of the name-object model, assignment never copies an object’s value; it only copies a reference to the object. This leads to a common pitfall when working with mutable objects: if you have two names pointing to the same mutable object, modifying it through one name affects the other.

list_a = [1, 2, 3]
list_b = list_a   # Both names now reference the SAME list object
list_b.append(4)  # Modifying the object via 'list_b'
print(list_a)     # Output: [1, 2, 3, 4] -> 'list_a' sees the change!

To avoid this, you must explicitly create a copy if you need an independent object.

list_a = [1, 2, 3]
list_b = list_a.copy()  # or list_a[:] — creates a new list object
list_b.append(4)
print(list_a)  # Output: [1, 2, 3] (unchanged)
print(list_b)  # Output: [1, 2, 3, 4]

Small Integer Caching and Interning

For optimization, the Python interpreter caches small integers (typically between -5 and 256) and interns certain strings (like those that look like identifiers). This means these objects are singletons; only one instance is created in memory and reused. This is why the is operator can behave unexpectedly for beginners.

a = 100
b = 100
print(a is b)  # Output: True (likely uses cached integer)

c = 1000
d = 1000
print(c is d)  # Output: False (typically, outside the cached range)
# However, you should use `==` for value comparison to avoid relying on this implementation detail.

Passing Arguments to Functions

Function arguments are passed using a mechanism called “pass-by-assignment.” Since assignment just copies references, the behavior depends on the object’s mutability. Passing a mutable object to a function allows the function to modify the original object. Passing an immutable object means the function can only rebind the parameter name to a new object, leaving the original unchanged.

def modify_arg(mutable_list, immutable_int):
    mutable_list.append(4)  # In-place modification affects the original
    immutable_int += 10     # Rebinds the local name 'immutable_int'. Original is unchanged.
    return immutable_int

my_list = [1, 2, 3]
my_int = 5
new_int = modify_arg(my_list, my_int)

print(my_list)  # Output: [1, 2, 3, 4] -> Was modified by the function
print(my_int)   # Output: 5 -> Unchanged
print(new_int)  # Output: 15