The tuple data type  represents an ordered, immutable collection of elements.  Tuples, in Python, share a lot of similarities with lists, however, unlike lists,  tuples cannot be updated once they are created. This makes them suitable for working with fixed sets of data such as coordinates, dates and any other pieces of data that won't change.

In Python, a tuple is simply a sequence of values that are separated by commas. Conventionally, we enclose the elements of a tuple in parentheses to enhance code readability. This makes it easy to distinguish tuples from other data types such as lists, which use square brackets. For example, both of the following tuples are equivalent:

T = 1, 2, 3, 4, 5
T1 = (1, 2, 3, 3, 4, 5)

To initialize an empty tuple, we simply use empty parentheses:

t = ()

We can also use the builtin tuple function, which returns an empty tuple if called without any argument, as shown below:

t = tuple()

Other examples: 

(1, 2, 3)#tuple of integers
('PHP', 'Javascript', 'Html', 'CSS')#tuple of strings
((1.0, 2.0), (3.0, 4.0), (5.0, 6.0))#tuple of tuples

It is, however, worth noting that parentheses are also used in expressions. Example:

(1 + 3)
(2 * 4)
( (2 + 2) * 5) 

This introduces room for ambiguity in some use cases, for example a tuple with only one element  will need to have a trailing comma, otherwise, it will be interpreted as an expression.Example:

(8, )
('Python', )

Consider in the examples above if we  did not add a trailing comma:

t = (8)
print( type(t) )

t2 = ('Python')
print( type(t2) )

Adding the trailing comma effectively makes it a tuple with a single element:

t = (8, )
print( type(t) )

t2 = ('Python', )
print( type(t2) )

Operations on Tuples

Many operations that can be performed on lists, such as concatenation, repetition, and indexing, can also be performed on tuples. However, because tuples are immutable, any operation that appears to modify a tuple actually creates a new tuple, whereas with lists, modifications can be made to the original list without creating a new one.

Concatenation and Repetition

The "+"operator is used to concatenate two  tuples. The syntax is as follows:

tuple1 + tuple2

This operation leads to creation of a new tuple which contains all the elements of tuple1 followed by all the elements of tuple2

print( (1, 2) + (3, 4) )

print( ('PHP', 'Javascript', 'CSS') + ('Python', 'Java', 'C++') )

Repetition, on the other hand, is achieved using the "*" operator. The operator, in this case, takes two operand , the tuple and an integer which specifies the number of time that the tuple will be repeated.  The operation results in a new tuple whose elements are the elements of the tuple operand repeated the number of times specified by the integer operand. 

T = (0, )
print( T * 5 )

T = ('Python', 'Java')
print( T * 2 )

T = ((1,3),)
print( 3 * T )

Tuple Length

The length of a tuple is the total number of elements it contains. For example an empty tuple will have a length of 0, a tuple with one element will have a length of and so forth. We can get the length of a given tuple using the builtin len() function. 

T = ()
print( len(T) )

T  = (1, )
print( len(T) )

T = (1, 2, 3, 4, 5)
print( len(T) )

Indexing and Slicing

Like other sequences in Python, you can use indexing to access individual elements in a tuple  through  their index, and  slicing is used to get all elements within a given range of indices.

Indices in Python sequences, including tuples, always starts at 0. The first element in the tuple has an index 0 , the second has an index 1, and so on.

In addition to the positive indices, each element  in a tuple also has a negative index. In this case the indices starts from the back of the tuple and ,move to the front. The last element in the tuple has an index of  -1 , the second last has an index of  -2 , the third last has an index of  -3 , and so on.

The square brackets [] are used for indexing, as well as slicing. In indexing, if we have a tuple T, the syntax is as follows:

T[ index ]

This returns the element at the specified index . 

Using positive indexing 

T = ('Python', 'Javascript', 'C++', 'Perl', 'Html', 'CSS', 'Kotlin', 'Java')

print( T[0] )  #get the first element
print( T[1] ) #get the second element
print( T[4] ) #get the fifth element
print( T[ len(T) - 1 ] ) #get the last element

In the following examples , we use negative indexing.

T = ('Python', 'Javascript', 'C++', 'Perl', 'Html', 'CSS', 'Kotlin', 'Java')

print( T[-1] ) #get the last element

print( T[-2] ) #get the second-last element

print( T[-3] ) #get the third-last element

print( T[ -len(T) ] ) #get the first element

Slicing ,as we said, is used to access the elements  in a given range of indices. If we have tuple T, the syntax is as follows:

T[start : stop : step]

Slicing returns a new tuple that contains all the elements of the original tuple whose indices begin at start and end at stop, without including the value at stop. The step value determines the interval between the selected indices.

if any of the three slicing parameters is omitted, Python will use default values; start defaults to 0, stop defaults to the length of the tuple, and step defaults to 1.

T = (1, 2, 3, 4, 5, 6, 7, 8, 9)

print( T[ : 5] )  #start defaults to 0 and step to 1
print( T[4 : ] ) #stop defaults to tuple's length and step 1
print( T[ 0:: 2 ] )  #stop  defaults to the tuple's length
print( T[ : ] ) #ommiting all parameters results in the same tuple
print( T[ :: -1 ] )  #using negative step to reverse the tuple

It is ,however,  worth noting that we cannot perform index or slice assignments in tuples like in lists and other mutable types. Doing this will raise a TypeError as shown below:

T = (1, 2, 3, 4, 5, 6, 7, 8, 9)

T[0] = 0 #TypeError: 

However , in cases where a tuple contains a mutable data type such as a list as its element, the mutable object can be changed without necessarily changing the tuple itself.

T = ([1, 2], [3, 4], [5, 6])

T[0][0] = 9
T[0][1] = 9

print( T )

In the above case, there is no creation of a new tuple since we are not changing the tuple itself but rather the list at index 0 of the tuple. This is because the tuple stores references to its elements and not the actual data.

Membership Operations

We can check whether a tuple contains a given element using the in operator. The syntax is as follows:

x in <tuple>

This operation returns True if element x is in the tuple, otherwise False.

T = ('Javascript', 'Python', 'C++', 'Perl', 'Html', 'CSS', 'Kotlin', 'Java')

print( 'Python' in T)
print( 'C#' in T )

The not in operator is the opposite of the in operator, it returns True if the item is not in the tuple and False otherwise. The syntax is as follows:

x not in <tuple>
T = ('Javascript', 'Python', 'C++', 'Perl', 'Html', 'CSS', 'Kotlin', 'Java')

print( 'Python' not in T )
print( 'C#' not in T )

Casting other types to tuples

The builtin  tuple() function can be used with a value from sequential types such as lists sets and strings  in order to turn them to tuples.

print( tuple( [1, 2, 3, 4, 5] ) ) #list to tuple

print( tuple( {'Python', 'PHP', 'CSS', 'Html'} ) ) #set to tuple

print( tuple('Python') ) #string to tuple

Tuple Methods

Due to their immutable nature, tuples support a limited set of methods. 

index()

Syntax:

T.index(x)

This method returns the index of the leftmost occurrence of in tuple T. A ValueError is raised if x is not found in the tuple.

T = ('Python', 'CSS', 'Javascript', 'C#', 'C++', 'Java')

print( T.index('Python') )
print( T.index('Java') )
print( T.index('C#') )
print( T.index('PHP') ) #ValueError

count()

Syntax:

T.count(x)

Returns the number of times that item occurs in tuple T.  

T = ('Python', 'CSS', 'Javascript', 'Python', 'C#', 'C++', 'Java',  'CSS', 'Python' )

print( T.count('Python') )
print( T.count('C++') )
print( T.count('CSS') )
print( T.count('PHP') )