Fundamentals 9 min read

Mutable vs Immutable Types in Python and Their Use in Multithreaded Programming

This article explains the memory‑management differences between mutable and immutable Python objects, demonstrates how each type behaves with code examples, and shows how immutability improves thread safety while providing practical synchronization techniques for mutable data in concurrent programs.

Test Development Learning Exchange
Test Development Learning Exchange
Test Development Learning Exchange
Mutable vs Immutable Types in Python and Their Use in Multithreaded Programming

In Python, mutable and immutable types differ in how memory is managed: mutable objects (e.g., lists) have a single in‑memory instance that can be altered in place, while immutable objects (e.g., tuples) require a new allocation for any change, making them safer for copying and multithreaded use.

Example of a mutable list shows that the object's id remains constant after modifying elements or appending new items:

list1 = [1, 2, 3]
print(id(list1))  # view list id
list1[0] = 'one'
print(id(list1))  # same id
list1.append(4)
print(id(list1))  # same id

Immutable tuple example demonstrates that attempting to modify raises an error and that creating a new tuple yields a different id :

tup1 = ('a', 'b', 'c')
print(id(tup1))
# tup1[0] = 'A'  # TypeError
tup2 = ('d', 'e', 'f')
print(id(tup2))

Mutable dictionary behaves like a list: its id stays the same after updating values or adding new keys:

dict1 = {"key": "value"}
print(id(dict1))
dict1["key"] = 'another value'
print(id(dict1))
dict1["new_key"] = 'new value'
print(id(dict1))

Immutable strings cannot be altered; creating a new string produces a new object:

str1 = "Hello"
print(id(str1))
# str1[0] = 'X'  # TypeError
str2 = "Goodbye"
print(id(str2))

Characters are just one‑character strings; concatenating creates a new string object, while trying to reassign a character raises an error:

char1 = 'H'
char2 = 'h'
concat_char = char1 + char2
print(id(concat_char))

Immutable data offers strong advantages in multithreaded environments because it cannot change, eliminating race conditions and allowing safe sharing and caching for performance gains.

A naïve counter updated by multiple threads without synchronization produces an incorrect result:

import threading
counter = 0
def increment_counter():
    global counter
    for _ in range(10_000):
        counter += 1
# create and start threads...
print(counter)  # not the expected value

Using a lock (or other atomic primitive) fixes the problem:

counter_lock = threading.Lock()
def increment_counter():
    global counter, counter_lock
    for _ in range(10_000):
        with counter_lock:
            counter += 1
# start threads and join
print(counter)  # expected result

A simple reader/writer example shows concurrent access to a shared dictionary, illustrating the need for proper synchronization.

import threading
data = {}
def reader():
    while True:
        print(f"Reader: {data.get('key')}")
def writer():
    while True:
        data['key'] = len(data)
        print(f"Writer: {len(data)}")

Cache usage with a lock ensures that expensive computations are performed only once and shared safely across threads:

import threading
cache = {}
lock = threading.Lock()
def get_cached_data(key):
    if key in cache:
        return cache[key]
    with lock:
        if key in cache:
            return cache[key]
        result = expensive_computation()
        cache[key] = result
        return result

Atomic integer (or similar atomic primitive) provides lock‑free thread‑safe increments:

import threading
with atomic_int = AtomicInteger(0):
    def increment():
        atomic_int.increment()
    # start multiple threads
    print(atomic_int.value())  # expected result

Explicit lock usage with mutex.acquire() / release() demonstrates another way to protect mutable shared state:

import threading
mutex = threading.Lock()
count = 0
def increment():
    global count
    mutex.acquire()
    try:
        count += 1
    finally:
        mutex.release()

When mutable data is accessed concurrently without synchronization, race conditions occur, as shown by a shared number being incorrectly updated; applying a lock resolves the issue and yields the correct final value.

import threading
shared_number = 0
def change_number():
    global shared_number
    shared_number += 1
# without lock, final value is unexpected
# with mutex = threading.Lock()
# acquire, modify, release
print(shared_number)  # expected result after locking
pythonconcurrencyThread SafetyfundamentalsImmutableMutable
Test Development Learning Exchange
Written by

Test Development Learning Exchange

Test Development Learning Exchange

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.