How to implement immutable Data structures in Python?

You need to implement immutable data structures in Python.
Immutable data structures are very handy when you want to prevent multiple people modifying a piece of data in parallel programming at same time. Mutable data structures( e.g. Array) can be changed at any time while immutable data structures cannot be.
Let me show you step by step how to deal with immutable and mutable data structures.
# STEP 01 - Create a Mutable array.

# Define an array
atp_players = ['Murray', 'Nadal', 'Djokovic']
print(f" *** Original Data in my array is - {atp_players}")
*** Original Data in my array is -
['Murray', 'Nadal', 'Djokovic']
# Changing the player name from Murray to Federer
atp_players[0] = 'Federer'
print(f" *** Modified Data in my array is - {atp_players}")
*** Modified Data in my array is -
['Federer', 'Nadal', 'Djokovic']
Conclusion 
We have been able to change the array value just like that which might be useful if you are an exclusive user of this array. However, in realtime production multiple programs might be using this array for changes and might result in unexpected data.
Tuples on other hand behaves a bit differently, have a look at below example.
# STEP 02 - Try changing a Tuple

try:
atp_players_tuple = ('Murray', 'Nadal', 'Djokovic')
print(f" *** Original Data in my tuple is - {atp_players_tuple}")
atp_players_tuple[0] = 'Federer'
except Exception as error:
print(f" *** Tried modifying data but ended up with - {error}")
*** Original Data in my tuple is - ('Murray', 'Nadal', 'Djokovic')
*** Tried modifying data but ended up with - 'tuple' object does not support item assignment
Conclusion :
What you have seen above is, tuple cannot be modified right ?. However there is one exception, if there are arrays with in a tuple the values can be changed.
atp_players_array_in_tuple = (['Murray'], ['Nadal'], ['Djokovic'])
print(f" *** Original Data in my tuple with arrays is - {atp_players_array_in_tuple}")

atp_players_array_in_tuple[0][0] = 'Federer'
print(f" *** Modified Data in my tuple with arrays is - {atp_players_array_in_tuple}")
*** Original Data in my tuple with arrays is - (['Murray'], ['Nadal'], ['Djokovic'])
*** Modified Data in my tuple with arrays is - (['Federer'], ['Nadal'], ['Djokovic'])
So how to protect the data ? Hmm, just by converting the arrays to tuples.
try:
atp_players_tuple_in_tuple = (('Murray'), ('Nadal'), ('Djokovic'))
print(f" *** Original Data in my tuple is - {atp_players_tuple_in_tuple}")
atp_players_tuple_in_tuple[0] = 'Federer'
except Exception as error:
print(f" *** Tried modifying data in my tuple but ended up with - {error}")
*** Original Data in my tuple is - ('Murray', 'Nadal', 'Djokovic')
*** Tried modifying data in my tuple but ended up with - 'tuple' object does not support item assignment
There is more.. Python has a wonderful built in tool called NamedTuple. It's a class you can extend to create a constructor. Let us understand programatically.
# Create a simple class on Grandslam titles in pythons way.
class GrandSlamsPythonWay:
def __init__(self, player, titles):
self.player = player
self.titles = titles

stats = GrandSlamsPythonWay("Federer", 20)
print(f" *** Stats has details as {stats.player} - {stats.titles}")
*** Stats has details as Federer - 20
What do you think of this class, is it immutable ? Let us check it out by changing Federer to Nadal.
stats.player = 'Nadal'
print(f" *** Stats has details as {stats.player} - {stats.titles}")
*** Stats has details as Nadal - 20
So no surprise it is a immutable data structure as we are able to Update Federer to Nadal. Now let us create a class with NamedTuple and see what is it's default behaviour.
from typing import NamedTuple

class GrandSlamsWithNamedTuple(NamedTuple):
player: str
titles: int

stats = GrandSlamsWithNamedTuple("Federer", 20)
print(f" *** Stats has details as {stats.player} - {stats.titles}")

stats.player = 'Djokovic'
print(f" *** Stats has details as {stats.player} - {stats.titles}")
*** Stats has details as Federer - 20
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
in
10 print(f" *** Stats has details as {stats.player} - {stats.titles}")
11
---> 12 stats.player = 'Djokovic'
13 print(f" *** Stats has details as {stats.player} - {stats.titles}")

AttributeError: can't set attribute
Looks like Djokovic had to wait some more time to reach 20 Grandslam titles.
However we can make use of _replace method to make a copy and update the values during the _replace.
djokovic_stats = stats._replace(player="Djokovic", titles=17)
print(f" *** djokovic_stats has details as {djokovic_stats.player} - {djokovic_stats.titles}")
*** djokovic_stats has details as Djokovic - 17
Finally i will put one example which covers everything explained above.
For this example, pretend we're writing software for vegetable shop.
from typing import Tuple

# Create a Class to represent one purchase
class Prices(NamedTuple):
id: int
name: str
price: int # Price in dollars

# Create a Class to track the purchase
class Purchase(NamedTuple):
purchase_id: int
items: Tuple[Prices]

# Create vegetable items and their corresponding prices
carrot = Prices(1, "carrot", 2)
tomato = Prices(2, "tomato", 3)
eggplant = Prices(3, "eggplant", 5)

# Now let say our first cusotmer Mr.Tom had purchased carrot and tomato
tom_order = Purchase(1, (carrot, tomato))

# Know the total cost we need to charge Mr.Tom
total_cost = sum(item.price for item in tom_order.items)
print(f"*** Total Cost from Mr.Tom is - {total_cost}$")
*** Total Cost from Mr.Tom is - 5$