Does Python have an immutable list?
Asked Answered
S

9

132

Does python have immutable lists?

Suppose I wish to have the functionality of an ordered collection of elements, but which I want to guarantee will not change, how can this be implemented? Lists are ordered but they can be mutated.

Suomi answered 21/6, 2012 at 16:15 Comment(1)
The main motivation for immutable types in Python is that they are usuable as dictionary keys and in sets.Lathing
S
131

Yes. It's called a tuple.

So, instead of [1,2] which is a list and which can be mutated, (1,2) is a tuple and cannot.


Further Information:

A one-element tuple cannot be instantiated by writing (1), instead, you need to write (1,). This is because the interpreter has various other uses for parentheses.

You can also do away with parentheses altogether: 1,2 is the same as (1,2)

Note that a tuple is not exactly an immutable list. Click here to read more about the differences between lists and tuples

Suomi answered 21/6, 2012 at 16:15 Comment(14)
Also, if you place inherently mutable object pointers in the tuple (e.g. ([1,2],3)), the tuple is no longer truly immutable, because the list object is just a pointer to a mutable object, and while the pointer is immutable, the referenced object is not.Zimmermann
@nadirs: RichieHindle made a comment to this effect, but it's now deleted. I should have realised, due to all the reading of the grammar I've been doing.Christlike
Actually an empty tuple can also be written (). That's the one case where the parentheses are required.Aquaplane
Immutable lists and tuples are two very different things. Tuples are immutable but they cannot be iterated over, and you cannot do map/reduce/filter/... on a single tuple, but you should be able to do that on a single mutable/immutable list. Check out other languages that promote immutability and functional programming more seriously than python... and you will find that immutable list is a must.Predacious
@Kane, your statement is certainly true in typed functional languages; specifically, (3,4,5) has a very different type—(int x int x int)—than [3,4,5], which has type (listof int). However, python's tuple really does seem closer to an immutable list: specifically, they can be iterated over, and it appears they can also be filtered and mapped.Tiffa
A tuple is not a List, they do not have compatible behavior, nor can you use them polymorphically.Tentmaker
@Tentmaker Thanks for the input. Have updated the answer to make your point a little more clearly.Suomi
Tuple is a list means that all elements of the same type (ok, python collections are typeless) and you have an operator for at least adding the elements. Yes, you should be able to add the elements, and even better to remove them, to the immutable list. Thanks for demonstrating this and proving that tuple is a true immutable list.Scherzando
This is one of the big problems with Python: it's so simple to use that nobody uses it correctly. (And if you try to use it correctly, you find out you can't.) The question was whether there exists an immutable list in Python. The accepted answer was, "use a tuple". But then pretty much everyone who understands the semantics agrees that a tuple is not a list. (Guido said so himself.) So at the end of the day the question is unanswered: how can I get an immutable list, the analog of frozenset for the set type?Bashemath
Agreed 'tuple' is not an highly accurate answer, but is it good enough for the vast majority? Is the answer inaccurate enough that it causes problems. If you are asking this question, and searching on SO for an answer, then this answer will probably suffice. If you're advanced enough that you are reading the docs, following Guido, and possibly reading the source, then you probably won't find yourself here. Still, I can empathise with the sentiment. There is far too much magic is software, and indeed the documentation of it.Suomi
a tuple doesn't support so many operations as a list does, and doesn't provide the immutable version. eg, what if I want to append an element to a tuple and get a new tuple?Trifacial
If you want type annotations, this falls flat on its face.Varrian
...so the answer I'm looking for is "no", which you seem to have misspelled.Retuse
This is completely useless if you can't guarantee that None of the executed code performs an isinstance(obj, list) or isinstance(obj, tuple) check that leads to a different code path. A prominent example would be pandas. (Hence pandas actually implements a FrozenList internally)Toback
A
26

This question deserves a modern answer, now that type annotations and type checking via mypy are getting more popular.

Replacing a List[T] by a tuple may not be the ideal solution when using type annotations. Conceptually a list has a generic arity of 1, i.e., they have a single generic argument T (of course, this argument can be a Union[A, B, C, ...] to account for heterogeneously typed lists). In contrast tuples are inherently variadic generics Tuple[A, B, C, ...]. This makes tuples an awkward list replacement.

In fact, type checking offers another possibility: It is possible to annotate variables as immutable lists by using typing.Sequence, which corresponds to the type of the immutable interface collections.abc.Sequence. For example:

from typing import Sequence


def f(immutable_list: Sequence[str]) -> None:
    # We want to prevent mutations like:
    immutable_list.append("something")


mutable_list = ["a", "b", "c"]
f(mutable_list)
print(mutable_list)

Of course, in terms of runtime behavior this isn't immutable, i.e., the Python interpreter will happily mutate immutable_list, and the output would be ["a", "b", "c", "something"].

However, if your project uses a type checker like mypy, it will reject the code with:

immutable_lists_1.py:6: error: "Sequence[str]" has no attribute "append"
Found 1 error in 1 file (checked 1 source file)

So under the hood you can continue to use regular lists, but the type checker can effectively prevent any mutations at type-check time.

Similarly you could prevent modifications of list members e.g. in immutable dataclasses (note that field assignment on a frozen dataclass in fact is prevent at runtime):

@dataclass(frozen=True)
class ImmutableData:
    immutable_list: Sequence[str]


def f(immutable_data: ImmutableData) -> None:
    # mypy will prevent mutations here as well:
    immutable_data.immutable_list.append("something")

The same principle can be used for dicts via typing.Mapping.

Aimeeaimil answered 28/9, 2021 at 8:47 Comment(3)
Sequence doesn't exactly mark it as immutable, it just means it doesn't have methods available that can mutate it. The underlying variable can still be mutable.Sarmatia
@Sarmatia I know, that is why I have written: Of course, in terms of runtime behavior this isn't immutable, i.e., the Python interpreter will happily mutate immutable_list. If your CI enforces strict type checks, which many projects do these days, it gets relatively safe.Aimeeaimil
This is actually a really good answer, for a different question.Laws
L
13

Here is an ImmutableList implementation. The underlying list is not exposed in any direct data member. Still, it can be accessed using the closure property of the member function. If we follow the convention of not modifying the contents of closure using the above property, this implementation will serve the purpose. Instance of this ImmutableList class can be used anywhere a normal python list is expected.

from functools import reduce

__author__ = 'hareesh'


class ImmutableList:
    """
    An unmodifiable List class which uses a closure to wrap the original list.
    Since nothing is truly private in python, even closures can be accessed and
    modified using the __closure__ member of a function. As, long as this is
    not done by the client, this can be considered as an unmodifiable list.

    This is a wrapper around the python list class
    which is passed in the constructor while creating an instance of this class.
    The second optional argument to the constructor 'copy_input_list' specifies
    whether to make a copy of the input list and use it to create the immutable
    list. To make the list truly immutable, this has to be set to True. The
    default value is False, which makes this a mere wrapper around the input
    list. In scenarios where the input list handle is not available to other
    pieces of code, for modification, this approach is fine. (E.g., scenarios
    where the input list is created as a local variable within a function OR
    it is a part of a library for which there is no public API to get a handle
    to the list).

    The instance of this class can be used in almost all scenarios where a
    normal python list can be used. For eg:
    01. It can be used in a for loop
    02. It can be used to access elements by index i.e. immList[i]
    03. It can be clubbed with other python lists and immutable lists. If
        lst is a python list and imm is an immutable list, the following can be
        performed to get a clubbed list:
        ret_list = lst + imm
        ret_list = imm + lst
        ret_list = imm + imm
    04. It can be multiplied by an integer to increase the size
        (imm * 4 or 4 * imm)
    05. It can be used in the slicing operator to extract sub lists (imm[3:4] or
        imm[:3] or imm[4:])
    06. The len method can be used to get the length of the immutable list.
    07. It can be compared with other immutable and python lists using the
        >, <, ==, <=, >= and != operators.
    08. Existence of an element can be checked with 'in' clause as in the case
        of normal python lists. (e.g. '2' in imm)
    09. The copy, count and index methods behave in the same manner as python
        lists.
    10. The str() method can be used to print a string representation of the
        list similar to the python list.
    """

    @staticmethod
    def _list_append(lst, val):
        """
        Private utility method used to append a value to an existing list and
        return the list itself (so that it can be used in funcutils.reduce
        method for chained invocations.

        @param lst: List to which value is to be appended
        @param val: The value to append to the list
        @return: The input list with an extra element added at the end.

        """
        lst.append(val)
        return lst

    @staticmethod
    def _methods_impl(lst, func_id, *args):
        """
        This static private method is where all the delegate methods are
        implemented. This function should be invoked with reference to the
        input list, the function id and other arguments required to
        invoke the function

        @param list: The list that the Immutable list wraps.

        @param func_id: should be the key of one of the functions listed in the
            'functions' dictionary, within the method.
        @param args: Arguments required to execute the function. Can be empty

        @return: The execution result of the function specified by the func_id
        """

        # returns iterator of the wrapped list, so that for loop and other
        # functions relying on the iterable interface can work.
        _il_iter = lambda: lst.__iter__()
        _il_get_item = lambda: lst[args[0]]  # index access method.
        _il_len = lambda: len(lst)  # length of the list
        _il_str = lambda: lst.__str__()  # string function
        # Following represent the >, < , >=, <=, ==, != operators.
        _il_gt = lambda: lst.__gt__(args[0])
        _il_lt = lambda: lst.__lt__(args[0])
        _il_ge = lambda: lst.__ge__(args[0])
        _il_le = lambda: lst.__le__(args[0])
        _il_eq = lambda: lst.__eq__(args[0])
        _il_ne = lambda: lst.__ne__(args[0])
        # The following is to check for existence of an element with the
        # in clause.
        _il_contains = lambda: lst.__contains__(args[0])
        # * operator with an integer to multiply the list size.
        _il_mul = lambda: lst.__mul__(args[0])
        # + operator to merge with another list and return a new merged
        # python list.
        _il_add = lambda: reduce(
            lambda x, y: ImmutableList._list_append(x, y), args[0], list(lst))
        # Reverse + operator, to have python list as the first operand of the
        # + operator.
        _il_radd = lambda: reduce(
            lambda x, y: ImmutableList._list_append(x, y), lst, list(args[0]))
        # Reverse * operator. (same as the * operator)
        _il_rmul = lambda: lst.__mul__(args[0])
        # Copy, count and index methods.
        _il_copy = lambda: lst.copy()
        _il_count = lambda: lst.count(args[0])
        _il_index = lambda: lst.index(
            args[0], args[1], args[2] if args[2] else len(lst))

        functions = {0: _il_iter, 1: _il_get_item, 2: _il_len, 3: _il_str,
                     4: _il_gt, 5: _il_lt, 6: _il_ge, 7: _il_le, 8: _il_eq,
                     9: _il_ne, 10: _il_contains, 11: _il_add, 12: _il_mul,
                     13: _il_radd, 14: _il_rmul, 15: _il_copy, 16: _il_count,
                     17: _il_index}

        return functions[func_id]()

    def __init__(self, input_lst, copy_input_list=False):
        """
        Constructor of the Immutable list. Creates a dynamic function/closure
        that wraps the input list, which can be later passed to the
        _methods_impl static method defined above. This is
        required to avoid maintaining the input list as a data member, to
        prevent the caller from accessing and modifying it.

        @param input_lst: The input list to be wrapped by the Immutable list.
        @param copy_input_list: specifies whether to clone the input list and
            use the clone in the instance. See class documentation for more
            details.
        @return:
        """

        assert(isinstance(input_lst, list))
        lst = list(input_lst) if copy_input_list else input_lst
        self._delegate_fn = lambda func_id, *args: \
            ImmutableList._methods_impl(lst, func_id, *args)

    # All overridden methods.
    def __iter__(self): return self._delegate_fn(0)

    def __getitem__(self, index): return self._delegate_fn(1, index)

    def __len__(self): return self._delegate_fn(2)

    def __str__(self): return self._delegate_fn(3)

    def __gt__(self, other): return self._delegate_fn(4, other)

    def __lt__(self, other): return self._delegate_fn(5, other)

    def __ge__(self, other): return self._delegate_fn(6, other)

    def __le__(self, other): return self._delegate_fn(7, other)

    def __eq__(self, other): return self._delegate_fn(8, other)

    def __ne__(self, other): return self._delegate_fn(9, other)

    def __contains__(self, item): return self._delegate_fn(10, item)

    def __add__(self, other): return self._delegate_fn(11, other)

    def __mul__(self, other): return self._delegate_fn(12, other)

    def __radd__(self, other): return self._delegate_fn(13, other)

    def __rmul__(self, other): return self._delegate_fn(14, other)

    def copy(self): return self._delegate_fn(15)

    def count(self, value): return self._delegate_fn(16, value)

    def index(self, value, start=0, stop=0):
        return self._delegate_fn(17, value, start, stop)


def main():
    lst1 = ['a', 'b', 'c']
    lst2 = ['p', 'q', 'r', 's']

    imm1 = ImmutableList(lst1)
    imm2 = ImmutableList(lst2)

    print('Imm1 = ' + str(imm1))
    print('Imm2 = ' + str(imm2))

    add_lst1 = lst1 + imm1
    print('Liist + Immutable List: ' + str(add_lst1))
    add_lst2 = imm1 + lst2
    print('Immutable List + List: ' + str(add_lst2))
    add_lst3 = imm1 + imm2
    print('Immutable Liist + Immutable List: ' + str(add_lst3))

    is_in_list = 'a' in lst1
    print("Is 'a' in lst1 ? " + str(is_in_list))

    slice1 = imm1[2:]
    slice2 = imm2[2:4]
    slice3 = imm2[:3]
    print('Slice 1: ' + str(slice1))
    print('Slice 2: ' + str(slice2))
    print('Slice 3: ' + str(slice3))

    imm1_times_3 = imm1 * 3
    print('Imm1 Times 3 = ' + str(imm1_times_3))
    three_times_imm2 = 3 * imm2
    print('3 Times Imm2 = ' + str(three_times_imm2))

    # For loop
    print('Imm1 in For Loop: ', end=' ')
    for x in imm1:
        print(x, end=' ')
    print()

    print("3rd Element in Imm1: '" + imm1[2] + "'")

    # Compare lst1 and imm1
    lst1_eq_imm1 = lst1 == imm1
    print("Are lst1 and imm1 equal? " + str(lst1_eq_imm1))

    imm2_eq_lst1 = imm2 == lst1
    print("Are imm2 and lst1 equal? " + str(imm2_eq_lst1))

    imm2_not_eq_lst1 = imm2 != lst1
    print("Are imm2 and lst1 different? " + str(imm2_not_eq_lst1))

    # Finally print the immutable lists again.
    print("Imm1 = " + str(imm1))
    print("Imm2 = " + str(imm2))

    # The following statemetns will give errors.
    # imm1[3] = 'h'
    # print(imm1)
    # imm1.append('d')
    # print(imm1)

if __name__ == '__main__':
    main()
Lollygag answered 10/1, 2014 at 18:23 Comment(1)
This is useful, hareesh. But shouldn't methods that return a list really return an ImmutableList? This seems easy enough to do e e.g. line 167: def __add__(self, other): return ImmutableList(self._delegate_fn(11, other)). Is there a reason you didn't do that?Dictation
A
9

You can simulate a Lisp-style immutable singly-linked list using two-element tuples (note: this is different than the any-element tuple answer, which creates a tuple that's much less flexible):

nil = ()
cons = lambda ele, l: (ele, l)

e.g. for the list [1, 2, 3], you would have the following:

l = cons(1, cons(2, cons(3, nil))) # (1, (2, (3, ())))

Your standard car and cdr functions are straightforward:

car = lambda l: l[0]
cdr = lambda l: l[1]

Since this list is singly linked, appending to the front is O(1). Since this list is immutable, if the underlying elements in the list are also immutable, then you can safely share any sublist to be reused in another list.

Armindaarming answered 23/3, 2019 at 4:59 Comment(4)
How is this implementation more flexible than the native tuple (a,b,c) ?Unbolt
@Unbolt You're able to prepend to a singly-linked list, unlike a regular tuple, which is frozen. This is what makes them much more versatile and a staple in functional programming languages.Armindaarming
Thanks for your response. I'm still trying to understand the benefit of this implementation, since I can also prepend an element by creating a new tuple instance: (z,) + (a,b,c). Is it a matter of performance?Unbolt
@Unbolt Yep. In your case concatenating an element is O(n) with n being the length of the existing list, as you have to allocate a new tuple of size n+1 and then copy the elements over. Here, concat is O(1) as you just need to allocate a size-2 tuple with a pointer to the original "list".Armindaarming
I
5

But if there is a tuple of arrays and tuples, then the array inside a tuple can be modified.

>>> a
([1, 2, 3], (4, 5, 6))

>>> a[0][0] = 'one'

>>> a
(['one', 2, 3], (4, 5, 6))
Immediately answered 21/3, 2014 at 20:44 Comment(4)
There can't really be such a thing as a collection that makes its contents immutable, because you'd need a way to make an immutable copy of arbitrary objects. To do that, you'd have to copy the classes those objects belong to, and even the builtin classes that they reference. And still, the objects could refer to the filesystem, or to the network, or something else that will just always be mutable. So since we can't make an arbitrary object immutable, we have to be satisfied with immutable collections of mutable objects.Vernation
@JackO'Connor Not completely agree. It all depends on how you model the world: external mutability can always be modeled as states evolving in time, and instead of maintaining a single mutable state s I can always choose to refer to s_t which is immutable. "Immutable collection of immutable objects" <-- check out Huskell, Scala, and other functional programming languages. Before I start learning Python I used to believe Python has full support of immutability and fp from what I heard from others, but it turns out not true.Predacious
I should've said, there can't really be such a thing in Python. Python's immutability relies on the programmer respecting conventions (like _private_variables), rather than any enforcement from the interpreter.Vernation
A language like Haskell makes a lot more guarantees, though if the programmer really wanted to be evil, they could still write to /proc/#/mem or link against unsafe libraries or whatever to break the model.Vernation
F
1

List and Tuple have a difference in their working style.

In LIST we can make changes after its creation but if you want an ordered sequence in which no changes can be applied in the future you can use TUPLE.

further information::

 1) the LIST is mutable that means you can make changes in it after its creation
 2) In Tuple, we can not make changes once it created
 3) the List syntax is
           abcd=[1,'avn',3,2.0]
 4) the syntax for Tuple is 
           abcd=(1,'avn',3,2.0) 
      or   abcd= 1,'avn',3,2.0 it is also correct
Fulltime answered 25/7, 2018 at 17:27 Comment(0)
P
0

Example 1

def foo(array_of_strings):
    array_of_strings[0] = "Hello, Jerry"
     
bar = ['Hi', 'how', 'are', 'you', 'doing']
foo(bar)
print(bar)

Output:

['Hello, Jerry', 'how', 'are', 'you', 'doing']

With the help of slicing in python 3.x

Example 2 with slicing

def foo(array_of_strings):
    array_of_strings[0] = "Hello, Jerry"
     
bar = ['Hi', 'how', 'are', 'you', 'doing']
foo(bar[:])
print(bar)

Output:

['Hi', 'how', 'are', 'you', 'doing']
Pratique answered 27/10, 2023 at 5:1 Comment(0)
W
0

You can use FrozenList class from frozenlist module. To install it run:

$ pip install frozenlist

This class implements a list-like structure and it's mutable until freeze method is called, after which list modifications raise RuntimeError. frozenlist is hashable when it's frozen.

>>> from frozenlist import FrozenList
>>> fl = FrozenList([1, 2, 3])
>>> fl
<FrozenList(frozen=False, [1, 2, 3])>
>>> fl.append(4)
>>> fl
<FrozenList(frozen=False, [1, 2, 3, 4])>
>>> fl.freeze()
>>> fl.append(5)
Traceback (most recent call last):
  File "", line 1, in 
  File "frozenlist/_frozenlist.pyx", line 106, in frozenlist._frozenlist.FrozenList.append
  File "frozenlist/_frozenlist.pyx", line 28, in frozenlist._frozenlist.FrozenList._check_frozen
RuntimeError: Cannot modify frozen list.
>>> hash(fl)
590899387163067792
Wild answered 27/10, 2023 at 5:16 Comment(0)
P
-2

Instead of tuple, you can use frozenset. frozenset creates an immutable set. you can use list as member of frozenset and access every element of list inside frozenset using single for loop.

Palaeolithic answered 1/2, 2016 at 11:35 Comment(1)
frozenset requires its set members to be hashable, which a list is not.Growler

© 2022 - 2024 — McMap. All rights reserved.