biotech Django & Python

Python Data Types: Mutable vs Immutable & Modern Usage 2026

AK
Ali Kasımoğlu
07 May 2021 schedule 5 min read
Python Data Types: Mutable vs Immutable Guide - AnomixLabs
analytics

Insight Density

groups Target Audience: Intermediate
65 Score

Calculated by technical complexity and content density.

Last updated: April 2026 · AnomixLabs Engineering Team

In Python, everything is an object. Knowing whether these objects are mutable or immutable protects against unexpected bugs and performance issues — and helps you write better Python code.

1. Object Identity: Seeing with `id()`

Every object in Python has a unique identity (its memory address). When immutable objects are 'changed', a new object is created, while mutable objects retain the same identity:

x = 42
print(id(x))    # e.g.: 140234567890
x = x + 1
print(id(x))    # DIFFERENT id — new object created

my_list = [1, 2, 3]
print(id(my_list))  # e.g.: 140234599999
my_list.append(4)
print(id(my_list))  # SAME id — object modified, identity unchanged

2. CPython Integer Interning

CPython pre-creates and caches (interns) integers between -5 and 256. Numbers in this range share the same object on each use:

a = 100; b = 100
print(a is b)    # True — same object (interned)

a = 1000; b = 1000
print(a is b)    # False — different objects (>256)

# This is why you should use == for integer comparison, not is
# The 'is' operator is for identity comparison, not value

3. Immutable Types

Once created, their contents cannot be changed. Any 'modification' creates a new object:

Comparison of Python mutable and immutable data types

# int, float, complex, bool
n = 10           # immutable int
b = True         # bool, subclass of int

# str — characters cannot be changed
s = 'hello'
s[0] = 'H'       # TypeError!

# tuple — elements cannot be changed
t = (1, 2, 3)
t[0] = 99        # TypeError!

# frozenset — immutable version of set
fs = frozenset({1, 2, 3})
fs.add(4)        # AttributeError!

# bytes — immutable version of bytearray
b = b'hello'
b[0] = 72        # TypeError!

4. Mutable Types

# list
l = [1, 2, 3]
l.append(4)     # [1, 2, 3, 4] — same object
l[0] = 99       # [99, 2, 3, 4] — same object

# dict
d = {'name': 'Alice'}
d['age'] = 30   # {'name': 'Alice', 'age': 30}

# set
s = {1, 2, 3}
s.add(4)        # {1, 2, 3, 4}

# bytearray — mutable bytes
ba = bytearray(b'hello')
ba[0] = 72      # bytearray(b'Hello')

5. Critical Pitfall: Mutable Default Arguments

One of Python's most common sources of bugs. Using a mutable default argument in a function definition creates a single object shared across all calls:

# WRONG — created once in function definition, shared!
def add_item(item, my_list=[]):
    my_list.append(item)
    return my_list

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2]  ← Surprise!
print(add_item(3))  # [1, 2, 3]  ← Still the same list!

# CORRECT — default with None
def add_item_correct(item, my_list=None):
    if my_list is None:
        my_list = []
    my_list.append(item)
    return my_list

6. Shallow Copy vs. Deep Copy

import copy

original = [[1, 2], [3, 4]]

# Shallow copy — inner lists are shared!
shallow = original.copy()  # or original[:]
shallow[0].append(99)
print(original[0])  # [1, 2, 99] ← Affected!

# Deep copy — completely independent copy
original2 = [[1, 2], [3, 4]]
deep = copy.deepcopy(original2)
deep[0].append(99)
print(original2[0])  # [1, 2] ← Not affected

7. Memory Usage Comparison

import sys

# list vs tuple memory size (Python 3.12)
my_list = [1, 2, 3, 4, 5]
my_tuple = (1, 2, 3, 4, 5)

print(sys.getsizeof(my_list))   # 120 bytes
print(sys.getsizeof(my_tuple))  # 80 bytes  ← 33% smaller

# Memory optimization with __slots__
class Normal:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class Optimized:
    __slots__ = ['x', 'y']
    def __init__(self, x, y):
        self.x = x
        self.y = y

# When creating thousands of instances, __slots__ saves ~50% memory

8. Python 3.10+ Type Hints

# Python 3.10+ — union type with | operator
def process(value: int | str) -> str:
    return str(value)

# list, dict, tuple type hints (built-in since 3.9)
names: list[str] = ['Alice', 'Alice']
score_board: dict[str, int] = {'Alice': 95}

# Python 3.12 — type alias syntax
type Vector = list[float]
type Matrix = list[Vector]

def dot_product(v1: Vector, v2: Vector) -> float:
    return sum(a * b for a, b in zip(v1, v2))

9. Walrus Operator (:=) — Python 3.8+

# Assignment expression — assign and use in condition
while chunk := file.read(8192):
    process(chunk)

# Avoid repeated computation in list comprehension
results = [y for x in data if (y := compute(x)) > 0]

# Common use with re.match
import re
if m := re.match(r'(\d+)-(\d+)', text):
    start, end = m.group(1), m.group(2)

10. Match Statement — Python 3.10+

def http_message(code: int) -> str:
    match code:
        case 200: return 'OK'
        case 404: return 'Not Found'
        case 500: return 'Server Error'
        case _: return 'Unknown'

# Structural pattern matching
def process_command(command):
    match command:
        case {'action': 'add', 'item': item}:
            return f'{item} added'
        case {'action': 'remove', 'item': item}:
            return f'{item} removed'
        case _:
            return 'Unknown command'

11. dataclass, TypedDict, and frozen

from dataclasses import dataclass, field
from typing import TypedDict

# dataclass — automatic __init__, __repr__, __eq__
@dataclass
class User:
    name: str
    age: int
    roles: list[str] = field(default_factory=list)

# frozen=True — immutable dataclass (hashable)
@dataclass(frozen=True)
class Coordinate:
    x: float
    y: float
    # x = 5 → FrozenInstanceError!

# TypedDict — type safety for dicts
class Movie(TypedDict):
    title: str
    year: int
    rating: float

Summary

Immutable: int, float, complex, bool, str, bytes, tuple, frozenset — hashable, can be used as dict keys, thread-safe.
Mutable: list, dict, set, bytearray — change in place, cannot be used as dict keys.
In modern Python: type-safe data structures with dataclass, dict guarantees with TypedDict, clean conditional logic with match, fluent expressions with the walrus operator :=, memory optimization with __slots__.

Frequently Questions

Why is a tuple faster than a list? expand_more
Tuples are immutable, so the Python interpreter places them in a fixed memory block. Lists require extra memory management for dynamic growth. You can see this with sys.getsizeof(): a 5-element tuple is smaller than an equivalent list. For iteration-only use cases with no need to modify elements, tuple is the more performant and semantically clear choice.
What is the difference between dataclass and NamedTuple? expand_more
NamedTuple is immutable and a subclass of tuple — it supports index access like a tuple and is hashable. dataclass is mutable by default (make it immutable with frozen=True), participates in class hierarchies, supports __post_init__ for validation, and can have default factories. Use NamedTuple for simple value objects you want to treat as tuples; use dataclass for richer objects with behavior.
Is everything really an object in Python? expand_more
Yes. In Python, integers, floats, strings, functions, classes, and modules — everything is an object. Verify it: type(42).__mro__ shows the inheritance chain of int, and dir(42) shows all methods available on an integer instance. Functions have __name__, __doc__, and __code__ attributes. This uniformity is what makes Python's introspection and metaprogramming capabilities so powerful.
Should I prefer list comprehension or a generator? expand_more
Use a generator when you're processing elements one at a time rather than loading everything into memory. A list comprehension ([x*2 for x in range(1_000_000)]) loads the entire list into RAM; a generator ((x*2 for x in range(1_000_000))) yields one element at a time. For large datasets or pipelines, generators are significantly more memory-efficient.
When is frozenset used? expand_more
frozenset is the immutable version of set. It can be used as a dict key (unlike a regular set). Two main use cases: 1) when you need a hashable set — e.g., storing a set inside a tuple, 2) when you want to prevent accidental mutation of a set passed to a function. frozenset also supports all set operations (union, intersection, difference) just like a regular set.
TypedDict vs dataclass: when to use which? expand_more
TypedDict adds type safety to an existing dict structure — it's still a dict under the hood, JSON-serializable and dict-compatible. dataclass is a real class — you can add methods, inherit from it, make it frozen. Use TypedDict for API response shapes, config dicts, and JSON schemas. Use dataclass for domain objects with behavior, validation, or where you want type-checked attribute access instead of string-keyed lookup.
Why is the Python 3.12 type alias syntax important? expand_more
Python 3.12's 'type Vector = list[float]' syntax (PEP 695) creates an explicit type alias rather than a simple variable assignment. IDEs and mypy understand it as a true alias definition rather than an ordinary assignment, enabling better autocomplete, cleaner error messages, and more accurate type narrowing. If you're on Python 3.12+, prefer this syntax over the older TypeAlias annotation.
Tags: #Python #Veri Tipleri #Mutable #Immutable #Type Hints #dataclass #TypedDict #Walrus #Python 3.12
share

Share This Article

Support us by sharing this with your network.

AK

Ali Kasımoğlu

Full-stack Developer & Founder of AnomixLabs

A software developer specializing in the Python and Django ecosystem. Focuses on modern web architectures, AI integrations, and minimalist user experiences. Under the AnomixLabs umbrella, he aims to transform complex problems into lean and effective digital solutions.

psychology
psychology

Ask a Question About the Article

AnomixAI · Answers based on the article content

5 questions left
Only about article content 0/500
forward_to_inbox

The Future Decoded.

Join 5,000+ engineers and founders receiving the monthly briefing on enterprise AI, software architecture and digital transformation. No spam.