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'))