0% found this document useful (0 votes)
7 views29 pages

03 Python Lists and Tuples

The document provides an overview of sequence types in Python, focusing on lists, tuples, strings, and ranges, with a detailed explanation of mutability and immutability. It discusses core operations on lists, common pitfalls when modifying lists, and best practices for copying and iterating over lists. Additionally, it covers sorting methods, including the differences between 'sort()' and 'sorted()', and advanced sorting techniques using custom keys.

Uploaded by

yawar ayub
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
7 views29 pages

03 Python Lists and Tuples

The document provides an overview of sequence types in Python, focusing on lists, tuples, strings, and ranges, with a detailed explanation of mutability and immutability. It discusses core operations on lists, common pitfalls when modifying lists, and best practices for copying and iterating over lists. Additionally, it covers sorting methods, including the differences between 'sort()' and 'sorted()', and advanced sorting techniques using custom keys.

Uploaded by

yawar ayub
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 29

Topic : Sequence Types and Lists in Python

A sequence in Python is a generic term for any ordered, indexable


collection of items. Think of it as a container where items are stored in a
specific order, and you can access them using their position (index).

The primary built-in sequence types are:

 Lists (list): The workhorse. Mutable (can be changed).

 Tuples (tuple): Like lists, but immutable (cannot be changed).

 Strings (str): A sequence of characters. Immutable.

 Range (range): A sequence of numbers. Immutable.

Lists (list) are the most common and versatile sequence type. They are
defined by square brackets [] with comma-separated items.

Key Characteristics of Lists:

 Ordered: Items maintain the order in which they were added. [1, 2,
3] is different from [3, 2, 1].

 Mutable: You can add, remove, or change items after the list has
been created. This is their most important feature.

 Heterogeneous: A single list can contain items of different data


types (e.g., [1, "hello", 3.14, True]).

 Dynamic: They can grow and shrink in size.


Topic : Mutable vs. Immutable Objects (The Crucial
Concept)
This is the key to understanding how Python variables work.

Core Explanation

 Immutable Objects: Cannot be changed after creation. All the


fundamental types like int, float, bool, str, and tuple are immutable.

o When you do x = 10 and then x = 11, you are not changing


the integer 10. You are making the variable x point to a
completely new integer object, 11. The original 10 is
unchanged.

 Mutable Objects: Can be changed in-place after creation. The


primary examples are list, dict, and set.

o When you have my_list = [1, 2] and then


do my_list.append(3), you are modifying the original list
object in memory. The variable my_list still points to the same
object, which now contains [1, 2, 3].

# IMMUTABLE EXAMPLE (int)

x = 10

print(f"Initial ID of x: {id(x)}") # e.g., ...1456

x=x+1

print(f"ID of x after change: {id(x)}") # e.g., ...1488 (A new object!)

# MUTABLE EXAMPLE (list)

my_list = [1, 2, 3]

print(f"Initial ID of my_list: {id(my_list)}") # e.g., ...2344

my_list.append(4)

print(f"ID of my_list after change: {id(my_list)}") # e.g., ...2344 (The


SAME object!)

The Assignment "Gotcha"

When you assign a mutable object to another variable, you


are not making a copy. You are making both variables point to the exact
same object in memory.
# Both variables point to the SAME list

original_list = ['a', 'b', 'c']

new_list = original_list

print(f"Original list before change: {original_list}")

# Modify the list using the 'new_list' variable

new_list.append('d')

# The change is reflected in the original_list too!

print(f"Original list AFTER change: {original_list}") # Output: ['a', 'b', 'c',


'd']

print(f"ID of original_list: {id(original_list)}")

print(f"ID of new_list: {id(new_list)}") # The IDs are identical

Topic : Solving Mutability Issues with Copying


To avoid the "gotcha" above, you need to create an explicit copy.

 Shallow Copy: Creates a new container (a new list) but fills it


with references to the items in the original list.

o How: Use the .copy() method or full slicing [:].

o This is fine for simple lists (e.g., a list of numbers). But it's a
trap for nested lists.

 Deep Copy: Creates a new container and recursively creates copies


of all the objects inside it.

o How: Use the copy module's deepcopy() function.

o This is necessary when your list contains other mutable


objects (like other lists or dictionaries).
import copy

# --- SHALLOW COPY EXAMPLE ---

original = [1, 2, 3]

shallow = original.copy() # or shallow = original[:]

shallow.append(4)

print(f"Original (unaffected by shallow copy): {original}") # [1, 2, 3]

print(f"Shallow Copy: {shallow}") # [1, 2, 3, 4]

# --- THE SHALLOW COPY TRAP (Nested Lists) ---

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

shallow_nested = original_nested.copy()

# Modify an inner list of the shallow copy

shallow_nested[0].append(99)

# The change IS reflected in the original! Because the inner lists were not
copied.

print(f"Original Nested after change: {original_nested}") # [[1, 2, 99], [3,


4]]

print(f"Shallow Nested Copy: {shallow_nested}") # [[1, 2, 99], [3, 4]]

# --- DEEP COPY TO THE RESCUE ---

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

deep_nested = copy.deepcopy(original_nested_2)

# Modify an inner list of the deep copy

deep_nested[0].append(99)
# The original is now safe!

print(f"Original Nested 2 (unaffected by deep copy): {original_nested_2}")


# [[1, 2], [3, 4]]

print(f"Deep Nested Copy: {deep_nested}") # [[1, 2,


99], [3, 4]]

Placement POV: Basic Patterns Using Lists


Lists are the foundation for solving a vast range of algorithmic problems.
Here are some fundamental patterns interviewers look for.

Pattern 1: The Collector / Accumulator

This is the most common pattern. You initialize an empty list and use a
loop to fill it with results that meet certain criteria.

 Problem: Given a list of numbers, return a new list containing only


the even numbers.

Pattern 2: The Two-Pointer Technique

Extremely common for problems involving sorted lists. You use two index
pointers that move towards each other or in the same direction to find a
pair or a property.

 Problem: Given a sorted list of numbers, find if there is a pair that


sums up to a target k.

Pattern 3: In-Place Modification

These problems test your understanding of list mutability and your ability
to solve a problem without using extra memory (O(1) space complexity).

 Problem (LeetCode Classic 283): "Move Zeroes". Given a list of


integers, move all 0s to the end of it while maintaining the relative
order of the non-zero elements.

Topic : Core List Operations (A Deeper Look)


Appending vs. Inserting:

 append(item): Adds to the end. It's very fast, O(1) on average


(amortized).

 insert(index, item): Adds at a specific position. This is slow, O(n),


because all subsequent elements must be shifted one position to
the right.

Placement Point of View

 Complexity Matters: An interviewer asks, "How would you add


1000 items to a list?"

o Good Answer: "I would append() them in a loop. This is


efficient as each append is O(1) on average."

o Bad Answer: "I would insert(0, item) in a loop."

o Why it's bad: This is a classic performance trap. Inserting at


the beginning of a list 1000 times results in
an O(n²) algorithm, which is terrible. Knowing the time
complexity of basic operations is crucial.

Topic : Binding, Aliasing, and enumerate()

 Binding Different Names to the Same List (Aliasing):


As we saw with mutability, this is when you have two or more variables pointing to
the exact same list object in memory. This is not a copy.
 Iterating Over a List:
The standard way is the "for-each" loop, which is clean and readable.

fruits = ["apple", "banana", "cherry"]

for fruit in fruits:

print(f"I like to eat {fruit}s.")


enumerate() - The Superior Way to Iterate with an Index:
Often, you need both the item and its index.

 The "Old School" / "C-Style" way (AVOID THIS):

for i in range(len(fruits)):

print(f"Fruit at index {i} is {fruits[i]}")

This is clumsy, less readable, and considered "un-Pythonic."

 The enumerate() way (DO THIS):


enumerate() is a built-in function that takes an iterable and returns
a special enumerate object, which yields pairs of (index, item).

for index, fruit in enumerate(fruits):

print(f"Fruit at index {index} is {fruit}")

# You can even start the index from a different number

for index, fruit in enumerate(fruits, start=1):

print(f"Choice #{index}: {fruit}")

Topic : Removing Items from a List & A Common


Trap
 list.pop(index): Removes the item at the given index and returns
it.

o If no index is provided, it removes and returns the last


item (an O(1) operation, making lists usable as stacks).

o Popping from the beginning or middle is slow (O(n)).

 list.remove(value): Searches for the first occurrence of value and


removes it.

o It does not return the value.

o It raises a ValueError if the value is not found.

o This is an O(n) operation because it may have to scan the list


to find the value.

 del statement: A more general way to delete.

o del my_list[i]: Removes the item at index i.

o del my_list[i:j]: Removes a slice from the list.

The #1 Trap: Modifying a List While Iterating Over It


NEVER do this:

numbers = [1, 2, 3, 4, 5, 6]

# GOAL: Remove all odd numbers

# !!! THIS CODE IS BUGGY !!!

for num in numbers:

if num % 2 != 0:

numbers.remove(num)

print(numbers) # Unexpected Output: [2, 4, 6, 3, 5] --> Wait, what?

Why it fails: The for loop's internal counter gets confused. When you
remove an item, the list shrinks, and the indices of the subsequent items
shift. The loop then skips over the next item. For example, when it
removes 1, the list becomes [2, 3, 4, 5, 6]. The loop moves to the next
index (index 1), which now holds 3, effectively skipping over 2.

Topic : Improving the Code Further (The Right Way to Modify)

Scenario: We have a list of student scores. We need to remove any failing


scores (below 50), and then for the remaining passing scores, we want to
print their rank and score.

scores = [88, 45, 95, 72, 30, 99, 48, 85]

print(f"Original scores: {scores}")

# BAD: Modifying list while iterating

for score in scores:

if score < 50:

scores.remove(score)

# BAD: Using range(len()) for iteration

for i in range(len(scores)):

# Let's say we want to sort and give ranks

scores.sort(reverse=True) # Inefficiently sorting inside the loop!


print(f"Rank {i+1}: {scores[i]}")

# This code is buggy, inefficient, and hard to read.

# The final output will be wrong due to the sorting inside the loop.

The Professional "After" Code (Pythonic and Correct)

scores = [88, 45, 95, 72, 30, 99, 48, 85]

print(f"Original scores: {scores}\n")

# --- Step 1: Filter the data (Create a new list) ---

# BEST PRACTICE: Use a list comprehension to build a new list with the
desired data.

# This avoids the modification-while-iterating bug completely.

passing_scores = [score for score in scores if score >= 50]

print(f"Passing scores: {passing_scores}")

# --- Step 2: Process the filtered data ---

# Sort the new list ONCE, outside the loop. This is efficient.

passing_scores.sort(reverse=True)

print("\n--- Final Rankings ---")

# BEST PRACTICE: Use enumerate() for clean access to index and value.

for rank, score in enumerate(passing_scores, start=1):

print(f"Rank #{rank}: {score}")

Topic : sort() vs sorted() (The Fundamental


Difference)
 list.sort() - The Method:
o What it is: A method that belongs
exclusively to the list class. You can only call it
on a list object (my_list.sort()).
o What it does: It modifies the list in-place.
The original list is rearranged, and the method
itself returns None.
o When to use: When you no longer need the
original order of the list and want to save
memory by not creating a new one.

 sorted() - The Built-in Function:


o What it is: A built-in Python function that
can take any iterable as an argument (lists,
tuples, strings, dictionaries, etc.).
o What it does: It returns a new, sorted list,
leaving the original iterable completely
unchanged.
o When to use: When you need to preserve the
original order of your data or when you are
sorting an iterable that isn't a list (like a tuple).
# --- list.sort() ---
fruits = ["cherry", "apple", "banana"]
print(f"Before sort(): {fruits}, ID: {id(fruits)}")

result = fruits.sort() # Modifies the list in-place


print(f"After sort(): {fruits}, ID: {id(fruits)}") # ID is the
same
print(f"The return value of .sort() is: {result}") #
Output: None

# --- sorted() ---


vegetables = ("carrot", "artichoke", "beetroot") # A
tuple is immutable
print(f"\nOriginal tuple: {vegetables}")

sorted_vegetables = sorted(vegetables) # Returns a


new list

print(f"New sorted list: {sorted_vegetables}")


print(f"Original tuple remains unchanged:
{vegetables}")

Topic : Advanced Sorting


with key and reverse
This is where sorting becomes a
superpower. Both sort() and sorted() accept
two optional keyword arguments.
 reverse=True: Sorts the items in
descending order.
 key=<function>: This is the most
important argument. The key parameter
specifies a function to be called on each
element of the list prior to making
comparisons. The sorting is then done
based on the return values of this
function (the "keys"). This allows for
complex, custom sorting logic.
 A lambda function is very commonly
used for the key.
 Scenario: You have a list of students,
represented as dictionaries. You need to
sort them first by their score (highest to
lowest), and then by their name
(alphabetically) as a tie-breaker.
students = [
{'name': 'Charlie', 'score': 88},
{'name': 'Alice', 'score': 95},
{'name': 'Bob', 'score': 88},
{'name': 'David', 'score': 72}
]

# Sorting by score (descending)


sorted_by_score = sorted(students,
key=lambda student: student['score'],
reverse=True)
print("--- Sorted by Score (Descending) ---")
for student in sorted_by_score:
print(student)
# Output:
# {'name': 'Alice', 'score': 95}
# {'name': 'Charlie', 'score': 88}
# {'name': 'Bob', 'score': 88}
# {'name': 'David', 'score': 72}

# ADVANCED: Sorting by score (desc), then


name (asc) as a tie-breaker
# The key returns a tuple. Python sorts
tuples element by element.
# We use a negative score to simulate
descending order for a number.
sorted_complex = sorted(students,
key=lambda s: (-s['score'], s['name']))
print("\n--- Sorted by Score (Desc) and then
Name (Asc) ---")
for student in sorted_complex:
print(student)
# Output (Notice Bob now comes before
Charlie):
# {'name': 'Alice', 'score': 95}
# {'name': 'Bob', 'score': 88}
# {'name': 'Charlie', 'score': 88}
# {'name': 'David', 'score': 72}

Lambda Function
Lambda functions in Python are anonymous
functions (functions without a name) defined using the
lambda keyword. They are often used for small, short-
lived operations where defining a full function with def
would be overkill.
Syntax:
lambda arguments: expression
lambda: The keyword to define a lambda function.
arguments: Input parameters (can be multiple,
separated by commas).
expression: A single expression that is evaluated and
returned. (No need to use return).
add = lambda a, b: a + b
print(add(10, 20)) # Output: 30

Topic : else in Loops


Python loops (for and while) have an optional else block
that is unique among programming languages.
 The else block executes only if the loop
completes its entire sequence without being
terminated by a break statement.
This is incredibly useful for search operations. The
common pattern is: "Search for something in a loop. If
you find it, break. If the loop finishes and
you never found it (and thus never broke), then run
the else block."
# Before: Using a flag variable (common in other
languages)
my_list = [1, 3, 7, 9]
search_item = 5
found = False
for item in my_list:
if item == search_item:
print(f"Found {search_item}!")
found = True
break
if not found:
print(f"{search_item} was not in the list.")

# After: Using the Pythonic for-else block (cleaner)


my_list = [1, 3, 7, 9]
search_item = 5
for item in my_list:
if item == search_item:
print(f"Found {search_item}!")
break
else: # This 'else' belongs to the 'for' loop
print(f"{search_item} was not in the list.")
Topic : The "int list" Challenge & Other Built-in
Functions
The Challenge: User Input to Integer List
 Problem: Write a program that takes a single line
of input from the user, which contains space-
separated numbers (e.g., "10 55 8 3"). Convert this
string into a list of integers.

More Essential Built-in Functions


 len(iterable): Returns the number of items.

 sum(iterable): Sums the items (must be numeric).

 min(iterable), max(iterable): Find the minimum or maximum item.

 all(iterable): Returns True if all elements in the iterable are truthy.

 any(iterable): Returns True if at least one element is truthy.


 zip(iter1, iter2, ...): Combines multiple iterables into a single iterator
of tuples.

Placement Point of View

Don't reinvent the wheel. Interviewers want to see that you know and
use the tools Python provides. If you need a sum, use sum(). If you need
to check if all values are positive, use all(). Writing your own loops for
these tasks is inefficient and signals a lack of familiarity with the
language's standard library.

Built-in Functions Practice Questions

Practice Question 1: Data Validation Check

 Scenario: You receive a list of sensor readings. For the data packet
to be valid, two conditions must be met: 1) all readings must be
non-negative, and 2) there must be at least one reading greater
than 100.

 Task: Write a function is_data_valid(readings) that returns True if


the data is valid, and False otherwise. Use all() and any().

def is_data_valid(readings: list) -> bool:

if not readings: # Handle empty list edge case

return False

all_non_negative = all(r >= 0 for r in readings)

any_high_reading = any(r > 100 for r in readings)

return all_non_negative and any_high_reading

# Test cases

print(f"Valid case: {is_data_valid([10, 5, 101, 30])}") # True

print(f"Invalid (negative): {is_data_valid([10, -5, 101, 30])}") # False

print(f"Invalid (no high reading): {is_data_valid([10, 5, 99, 30])}") # False

print(f"Invalid (empty): {is_data_valid([])}") # False


Topic : Various Ways of Creating and Populating a
List
Beyond just my_list = [1, 2, 3], there are several powerful patterns for
creating lists that good Python developers use.

Core Explanation

1. List Literals []: The most direct way.


my_list = []

2. List Comprehensions: The most "Pythonic" and powerful way to


create a new list by transforming or filtering another iterable.
squares = [x**2 for x in range(10)] # [0, 1, 4, ..., 81]
evens = [x for x in range(10) if x % 2 == 0] # [0, 2, 4, 6, 8]

3. The list() Constructor: Useful for converting other iterables into a


list.
from_tuple = list((1, 2, 3)) # -> [1, 2, 3]
chars = list("hello") # -> ['h', 'e', 'l', 'l', 'o']

4. Initialization with a Default Value: Creates a list of a specific


size filled with a default value.
scores = [0] * 5 # -> [0, 0, 0, 0, 0]

The MUTABILITY TRAP in Initialization (Crucial for Interviews):


Be extremely careful when initializing with a mutable object.

# !!! BUGGY CODE !!!

nested_list = [[]] * 3

print(f"Before: {nested_list}") # [[], [], []]

# Modify what you think is just the first inner list

nested_list[0].append(1)

# Surprise! All inner lists are changed.

print(f"After: {nested_list}") # [[1], [1], [1]]


 Why? The * operator copies the reference to the inner list []. All
three positions in the outer list point to the exact same inner list
object in memory. This is a classic "gotcha" that interviewers use to
test your understanding of references and mutability.

 The Fix: Use a list comprehension to ensure a new inner list is


created for each position.
correct_nested_list = [[] for _ in range(3)]

Advanced Deletion: O(1) Removal from an Unsorted List

 Problem: You have a very large, unsorted list. You need to delete
an item from the middle by its index. A normal del or pop would be
O(n) because of the need to shift all subsequent elements.

 The Trick: If the order of the remaining elements doesn't matter,


you can achieve O(1) deletion.

1. Swap the item you want to delete with the last item in the
list.

2. Pop the last item off the list (which is an O(1) operation).

data = [10, 20, 30, 40, 50, 60]

index_to_delete = 2 # We want to delete '30'

# Standard slow way:

# del data[index_to_delete] # This is O(n)


# The O(1) trick for unsorted lists:

last_index = len(data) - 1

# Swap the item to delete with the last item

data[index_to_delete], data[last_index] = data[last_index],


data[index_to_delete]

# Pop the last item (which is now the one we wanted to delete)

data.pop()

print(data) # Output: [10, 20, 60, 40, 50] (order is changed, but it was
fast)

Topic : reversed() vs. list.reverse()

The Iterator Nuance: reversed() does not return a new list. It returns an
iterator, which is a memory-efficient object that yields items one by one.
To get a reversed list, you must pass the iterator to the list() constructor.

nums = [1, 2, 3, 4]

reversed_iterator = reversed(nums)

print(f"Original list: {nums}") # [1, 2, 3, 4]

print(f"The returned object is an: {type(reversed_iterator)}") # <class


'list_reverseiterator'>

# To get a list, you must explicitly convert it

reversed_list = list(reversed_iterator)

print(f"The new reversed list: {reversed_list}") # [4, 3, 2, 1]


Topic : Working with Nested Lists (Matrices)
A nested list is a list that contains other lists as
elements. This is the primary way to represent
2D data structures like matrices, game boards, or
tables.
Accessing Elements: Use double square
brackets: matrix[row][column].
Iteration: Use nested for loops. The outer loop
iterates through the rows, and the inner loop
iterates through the items (columns) in each row.
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]

# Access the number 5


element = matrix[1][1] # Row 1, Column 1
print(f"Element at [1][1] is: {element}")

# Iterate and print all elements


print("\nIterating through the matrix:")
for row in matrix:
for item in row:
print(item, end=" ")
print() # Newline after each row

Problem: Flatten a Nested List


 Scenario: You have data from multiple
sources, and it has come in as a list of lists.
You need to combine all the data into a
single, "flat" list for processing.
 Task: Write a
function flatten(nested_list) that takes a list
of lists and returns a single list containing all
the elements.
 Example: flatten([[1, 2], [3, 4, 5],
[6]]) should return [1, 2, 3, 4, 5, 6].

Problem: Matrix Diagonal Sum


 Scenario: You are analyzing a square matrix
representing a network's cost map. You need
to calculate the sum of the costs along the
two main diagonals.
 Task: Write a
function diagonal_sum(matrix) that takes a
square matrix (N x N list of lists) and returns
the sum of its primary diagonal (top-left to
bottom-right) and its secondary diagonal
(top-right to bottom-left).
 Example: For [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
the sum is (1+5+9) + (3+5+7) = 15 + 15 =
30. Notice the center element 5 is part of
both. You must handle this correctly (either
by including it twice as per the example or by
including it only once, a common
variation). Let's write a solution that avoids
double-counting the center element.

Problem: Transpose of a Matrix

Problem: Given a square matrix, find its transpose (rows become


columns and vice versa).

Problem: Rotate a Matrix by 90 Degrees (Clockwise)

Problem: Given a square matrix, rotate it by 90 degrees clockwise.

Example

Input:

123

456
789

Output:

741

852

963

Problem : Spiral Order Traversal of a Matrix

Problem: Given a 2D matrix, print its elements in spiral order.

Example:

Input:

1 2 3 4

5 6 7 8

9 10 11 12

Output: 1 2 3 4 8 12 11 10 9 5 6 7

Topic : split() and join() Methods - The Perfect


Pair
These two string methods are opposites, and they are your primary tools
for converting between strings and lists of strings.

str.split(separator)

 What it does: Breaks a string into a list of smaller strings, using


the separator to decide where to make the cuts.

 Most Important Feature: If you call .split() with no arguments, it


splits on any whitespace (spaces, tabs, newlines) and gracefully
handles multiple spaces between words. This is different and often
more useful than .split(' ').

separator.join(iterable)

 What it does: Joins the elements of an iterable (like a list) into a


single string, with the separator string placed between each
element.

 Syntax Trap: The syntax is often counter-intuitive for beginners.


The method is called on the separator string, not on the list.

 Crucial Rule: The iterable must contain only strings. Trying to join
a list of numbers will raise a TypeError.

word_list = ["Python", "is", "awesome"]

# Join them with a space

sentence = " ".join(word_list)

print(f"Joined with space: '{sentence}'") # Output: 'Python is awesome'

# Join them with a hyphen

kebab_case = "-".join(word_list)

print(f"Joined with hyphen: '{kebab_case}'") # Output: 'Python-is-


awesome'

# --- The TypeError Trap and The Fix ---

numbers = [1, 2, 3, 4, 5]

# print("-".join(numbers)) # This will raise a TypeError!

# The fix: Convert each item to a string first using a generator expression
or list comprehension

fixed_string = "-".join(str(num) for num in numbers)

print(f"Correctly joined numbers: '{fixed_string}'") # Output: '1-2-3-4-5'


Placement Point of View

 Performance: join() is the only professionally acceptable way to


build a string from many small parts in a loop. Using my_string +=
new_part repeatedly is O(n²) because strings are
immutable. "".join(list_of_parts) is an O(n) operation. Knowing this
performance difference is a major plus in an interview.

 Parsing: split() is the first step in almost any problem that involves
parsing data from a string (e.g., log files, CSV data, user input).

Topic : Tuple Introduction


A tuple is an ordered, indexable sequence of items, just like a list. The
single, most important difference is that tuples are immutable. Once a
tuple is created, you cannot change, add, or remove its elements.

 Syntax: Defined by parentheses () with comma-separated items.


The parentheses are optional in many cases; the commas are what
make it a tuple.

 Single-Element Tuple: To create a tuple with only one item,


you must include a trailing comma: my_tuple = (1,). Without the
comma, (1) is just the integer 1.

my_tuple = (1, "hello", True)

print(f"My tuple: {my_tuple}")

# Access by index (just like a list)

print(f"First element: {my_tuple[0]}") # Output: 1

# Attempting to change an element raises a TypeError

try:

my_tuple[0] = 99

except TypeError as e:

print(f"\nError: {e}") # Output: Error: 'tuple' object does not support


item assignment

# A single-element tuple

single_tuple = ("lonely",)
print(f"Type of ('lonely',): {type(single_tuple)}") # <class 'tuple'>

not_a_tuple = ("lonely")

print(f"Type of ('lonely'): {type(not_a_tuple)}") # <class 'str'>

Why Use Tuples? (Placement Perspective)

An interviewer asking "When would you use a tuple over a list?" is testing
your architectural thinking.

1. Data Integrity: Use tuples for data that should not be changed. For
example, a tuple of (latitude, longitude) for a fixed location. It
prevents accidental modification.

2. Dictionary Keys: Dictionary keys must be immutable. You can use


a tuple as a dictionary key, but you cannot use a list. This is a very
common use case. locations = {("New York", "NY"): "USA"}.

3. Returning Multiple Values from a Function: This is the most


"Pythonic" use case. Functions can easily return multiple results
packaged as a tuple, which can then be elegantly unpacked.

4. Performance: Tuples are slightly more memory-efficient and faster


to create than lists because their size is fixed. This is a micro-
optimization but shows deeper knowledge.

Topic 3: Unpacking Tuples and Other Sequences

Unpacking is the process of assigning the items of a sequence (tuple,


list, etc.) to multiple variables in a single statement. It's elegant and
highly readable.

1. Basic Unpacking: The number of variables must match the


number of items in the sequence.

# A function returning a tuple


def get_user_info():
return ("Alice", 30, "[email protected]")

# Unpack the returned tuple into variables


name, age, email = get_user_info()

print(f"Name: {name}, Age: {age}, Email: {email}")


2. Extended Unpacking (Using the * operator):
This is used when you want to unpack some items and collect
the "rest" into a new list.

numbers = [1, 2, 3, 4, 5, 6]

# Get the first, last, and everything in between


first, *middle, last = numbers
print(f"First: {first}") # 1
print(f"Middle: {middle}") # [2, 3, 4, 5] (This is a list!)
print(f"Last: {last}") #6

Topic : Nested Tuples and Lists (The Mutability


Puzzle)
This is an advanced concept that directly tests your understanding of how
Python's memory model works.

 The Rule: A tuple is immutable, meaning you cannot change the


objects it directly contains.

 The Puzzle: What if a tuple contains a mutable object, like a list?

You can't change which list the tuple holds, but you can change
the contents of that list.

# A tuple containing a mutable list

student_record = ("Bob", 22, ["Math", "Physics"])

print(f"Original record: {student_record}")

print(f"ID of inner list: {id(student_record[2])}")

# Try to assign a new list to the tuple -> FAILS

try:

student_record[2] = ["History"]

except TypeError as e:

print(f"\nError: {e}")
# Modify the inner list in-place -> SUCCEEDS!

student_record[2].append("Chemistry")

print(f"\nModified record: {student_record}")

print(f"ID of inner list is unchanged: {id(student_record[2])}")

Why this works: The tuple holds a reference to the list object.
The append method modifies the list object itself, but the tuple's
reference to that object remains unchanged. This is a subtle but critical
distinction.

Tuples Practice Question 1

 Scenario: You are processing employee records from a raw string.


Each record contains a name, an employee ID, and a comma-
separated list of skills. The string contains multiple records
separated by semicolons.

 Task:

1. Parse the raw string.

2. For each employee, create a tuple containing their name


(string), ID (integer), and skills (as a tuple of strings).

3. Filter out any employee whose ID is not 4 digits long.

4. Print the final list of valid employee record tuples.

 Raw
Data: "Alice:1234:python,java,sql;Bob:567:javascript,css;Charlie:89
10:python,aws,docker"

OUTPUT

--- Valid Employee Records ---

('Alice', 1234, ('python', 'java', 'sql'))

('Charlie', 8910, ('python', 'aws', 'docker'))

You might also like