Multidimensional Lists in Python

Understanding Multidimensional Lists

Multidimensional lists in Python are essentially lists nested within lists, creating structures that can represent data in multiple dimensions. The most common examples are 2D lists (tables/matrices) and 3D lists (cubes), but you can extend this concept to any number of dimensions.

Unlike languages with explicit multidimensional array support, Python handles multidimensional structures through nested lists. This approach offers flexibility, but comes with some considerations for memory management and performance.

Multidimensional lists are widely used in:

  • Matrix representation and mathematical operations
  • Grid-based applications (games, simulations)
  • Image processing (where images are 2D arrays of pixels)
  • Representing tabular data (rows and columns)
  • 3D modeling and simulations
# A 2D list (matrix) - 3x3
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# A 3D list (cube) - 2x2x2
cube = [
    [
        [1, 2],
        [3, 4]
    ],
    [
        [5, 6],
        [7, 8]
    ]
]

# Accessing elements
print(matrix[1][2])     # 6 - Row 1, Column 2
print(cube[1][0][1])    # 6 - Depth 1, Row 0, Column 1

Creating Multidimensional Lists

There are several ways to create multidimensional lists in Python. The method you choose depends on your specific needs and the complexity of your data structure.

Direct Initialization

The most straightforward way to create a multidimensional list is by directly nesting lists within lists.

# Direct initialization of a 2D list
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# Direct initialization of a 3D list
cube = [
    [[1, 2], [3, 4]],
    [[5, 6], [7, 8]]
]

# Non-rectangular structure (jagged array)
jagged = [
    [1, 2, 3],
    [4, 5],
    [6, 7, 8, 9]
]

# Mixed data types
mixed = [
    ["Name", "Age", "City"],
    ["Alice", 25, "New York"],
    ["Bob", 30, "San Francisco"]
]

Using List Comprehensions

List comprehensions offer a more compact and often more readable way to create multidimensional lists, especially when generating regular patterns or applying transformations.

# 2D list with list comprehension (3x3 matrix)
matrix = [[i*3 + j + 1 for j in range(3)] for i in range(3)]
print(matrix)  # [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# Empty matrix with specific dimensions (5x3)
empty_matrix = [[0 for _ in range(3)] for _ in range(5)]
print(empty_matrix)  # 5 rows, 3 columns, all zeros

# Identity matrix (diagonal of 1s, rest 0s)
identity = [[1 if i == j else 0 for j in range(4)] for i in range(4)]
print(identity)  # [[1,0,0,0], [0,1,0,0], [0,0,1,0], [0,0,0,1]]

# 3D list (2x3x2)
cube = [[[i+j+k for k in range(2)] for j in range(3)] for i in range(2)]
print(cube)  # 3D array with calculated values

Common Initialization Pitfalls

Be careful when using multiplication to create multidimensional lists. This approach can lead to unexpected behavior.

# INCORRECT way (creates references to the same inner list)
wrong_matrix = [[0] * 3] * 3
print(wrong_matrix)  # [[0, 0, 0], [0, 0, 0], [0, 0, 0]]

# Let's modify one element
wrong_matrix[0][0] = 1
print(wrong_matrix)  # [[1, 0, 0], [1, 0, 0], [1, 0, 0]] -- All rows changed!

# CORRECT way (creates independent inner lists)
correct_matrix = [[0 for _ in range(3)] for _ in range(3)]
correct_matrix[0][0] = 1
print(correct_matrix)  # [[1, 0, 0], [0, 0, 0], [0, 0, 0]] -- Only first row changed

# Another correct approach with nested loops
matrix = []
for i in range(3):
    row = []
    for j in range(3):
        row.append(i * 3 + j + 1)
    matrix.append(row)
print(matrix)  # [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Accessing and Modifying Elements

Working with elements in multidimensional lists requires using multiple indices to navigate through the nested structure. Understanding how to properly access and modify these elements is fundamental.

Accessing Elements

To access elements in a multidimensional list, use consecutive square brackets with the appropriate indices. The first index typically refers to the row (or outer list), the second to the column (or inner list), and so on.

# Sample 2D list (matrix)
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# Accessing individual elements
top_left = matrix[0][0]      # 1 (row 0, column 0)
middle = matrix[1][1]        # 5 (row 1, column 1)
bottom_right = matrix[2][2]  # 9 (row 2, column 2)

# Common indexing patterns
matrix[row][column]  # Access specific element
matrix[row]          # Access entire row (returns a list)

# Sample 3D list (cube)
cube = [
    [[1, 2], [3, 4]],
    [[5, 6], [7, 8]]
]

# Accessing elements in 3D
cube[depth][row][column]  # Indexing pattern
cube[0][1][0]             # 3 (depth 0, row 1, column 0)
cube[1][1][1]             # 8 (depth 1, row 1, column 1)

# Safe accessing with get (to prevent IndexError)
def safe_get(lst, indices, default=None):
    """Safely get an item from a nested list using multiple indices."""
    result = lst
    try:
        for idx in indices:
            result = result[idx]
        return result
    except (IndexError, TypeError):
        return default
        
print(safe_get(matrix, [5, 0], "Not found"))  # "Not found"
print(safe_get(matrix, [1, 1], "Not found"))  # 5

Modifying Elements

Since lists are mutable, you can modify elements in multidimensional lists using the same indexing syntax.

# Modifying individual elements
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

matrix[0][0] = 10          # Modify a single element
print(matrix)              # [[10, 2, 3], [4, 5, 6], [7, 8, 9]]

# Replacing an entire row
matrix[1] = [14, 15, 16]   # Replace the middle row
print(matrix)              # [[10, 2, 3], [14, 15, 16], [7, 8, 9]]

# Adding a new row
matrix.append([10, 11, 12])  # Add a fourth row
print(matrix)                # [[10, 2, 3], [14, 15, 16], [7, 8, 9], [10, 11, 12]]

# Modifying elements in a 3D list
cube = [
    [[1, 2], [3, 4]],
    [[5, 6], [7, 8]]
]
cube[0][0][1] = 20  # Modify a specific element
print(cube)         # [[[1, 20], [3, 4]], [[5, 6], [7, 8]]]

# Modifying all elements with nested loops
for i in range(len(matrix)):
    for j in range(len(matrix[i])):
        matrix[i][j] *= 2  # Double every element

# Modifying with list comprehension
matrix = [[cell * 0.5 for cell in row] for row in matrix]  # Halve every element

Iterating Through Multidimensional Lists

Iteration is a fundamental operation when working with multidimensional lists. Whether you need to process every element, search for specific values, or transform data, understanding iteration techniques is essential.

Basic Iteration

The most common way to iterate through multidimensional lists is using nested loops or nested comprehensions.

# Basic iteration through a 2D list with nested loops
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# Method 1: Row by row
for row in matrix:
    for element in row:
        print(element, end=" ")  # 1 2 3 4 5 6 7 8 9
    print()  # New line after each row

# Method 2: Using indices
for i in range(len(matrix)):
    for j in range(len(matrix[i])):
        print(f"matrix[{i}][{j}] = {matrix[i][j]}")

# Method 3: Using enumerate
for i, row in enumerate(matrix):
    for j, element in enumerate(row):
        print(f"Position ({i},{j}): {element}")

# Iteration through a 3D list
cube = [
    [[1, 2], [3, 4]],
    [[5, 6], [7, 8]]
]

for i, matrix in enumerate(cube):
    for j, row in enumerate(matrix):
        for k, element in enumerate(row):
            print(f"cube[{i}][{j}][{k}] = {element}")

Iteration with Comprehensions

List comprehensions provide a more concise way to process all elements in a multidimensional list.

# Flattening a 2D list into a 1D list
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened = [element for row in matrix for element in row]
print(flattened)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Transforming every element
squared = [[x**2 for x in row] for row in matrix]
print(squared)  # [[1, 4, 9], [16, 25, 36], [49, 64, 81]]

# Filtering elements
even_elements = [[x for x in row if x % 2 == 0] for row in matrix]
print(even_elements)  # [[2], [4, 6], [8]]

# Filtering rows
filtered_rows = [row for row in matrix if sum(row) > 10]
print(filtered_rows)  # [[4, 5, 6], [7, 8, 9]]

# Flattening and filtering in one step
flat_even = [x for row in matrix for x in row if x % 2 == 0]
print(flat_even)  # [2, 4, 6, 8]

Advanced Iteration Techniques

Sometimes you need to iterate in more complex patterns or use advanced iteration tools.

# Diagonal traversal
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# Main diagonal (top-left to bottom-right)
main_diagonal = [matrix[i][i] for i in range(len(matrix))]
print(main_diagonal)  # [1, 5, 9]

# Anti-diagonal (top-right to bottom-left)
anti_diagonal = [matrix[i][len(matrix)-1-i] for i in range(len(matrix))]
print(anti_diagonal)  # [3, 5, 7]

# Spiral traversal (complex pattern)
def spiral_order(matrix):
    result = []
    if not matrix:
        return result
    
    rows, cols = len(matrix), len(matrix[0])
    top, bottom = 0, rows - 1
    left, right = 0, cols - 1
    
    while top <= bottom and left <= right:
                    # Traverse right
            for j in range(left, right + 1):
                result.append(matrix[top][j])
            top += 1
            
            # Traverse down
            for i in range(top, bottom + 1):
                result.append(matrix[i][right])
            right -= 1
            
            # Traverse left
            if top <= bottom:
                for j in range(right, left - 1, -1):
                    result.append(matrix[bottom][j])
                bottom -= 1
            
            # Traverse up
            if left <= right:
                for i in range(bottom, top - 1, -1):
                    result.append(matrix[i][left])
                left += 1
            
    return result

print(spiral_order(matrix))  # [1, 2, 3, 6, 9, 8, 7, 4, 5]

Iterating Through Jagged Arrays

Jagged arrays (where rows have different lengths) require special consideration during iteration.

# Jagged array example
jagged = [
    [1, 2, 3],
    [4, 5],
    [6, 7, 8, 9]
]

# Safe iteration with row sizes
for i, row in enumerate(jagged):
    print(f"Row {i} has {len(row)} elements: {row}")

# Finding the maximum in each row
row_maxes = [max(row) for row in jagged]
print(row_maxes)  # [3, 5, 9]

# Safe transformation
doubled = [[x * 2 for x in row] for row in jagged]
print(doubled)  # [[2, 4, 6], [8, 10], [12, 14, 16, 18]]

Common Operations on Multidimensional Lists

There are several common operations you might need to perform on multidimensional lists, from basic mathematics to more complex matrix manipulations.

Matrix Transposition

Transposing a matrix means swapping rows with columns, essential in many mathematical operations.

# Original matrix
matrix = [
    [1, 2, 3],
    [4, 5, 6]
]  # 2x3 matrix

# Method 1: Transposing with list comprehension
transposed = [[row[i] for row in matrix] for i in range(len(matrix[0]))]
print(transposed)  # [[1, 4], [2, 5], [3, 6]] (now 3x2)

# Method 2: Using zip
transposed_zip = list(map(list, zip(*matrix)))
print(transposed_zip)  # [[1, 4], [2, 5], [3, 6]]

# The zip method is more concise but may be less intuitive to beginners
# The * operator unpacks the matrix as separate arguments to zip()

Matrix Mathematics

Implementation of basic matrix operations using nested lists.

# Matrix addition
def matrix_add(A, B):
    if len(A) != len(B) or len(A[0]) != len(B[0]):
        raise ValueError("Matrices must have the same dimensions")
    
    result = []
    for i in range(len(A)):
        row = []
        for j in range(len(A[0])):
            row.append(A[i][j] + B[i][j])
        result.append(row)
    return result

# Or more concisely with comprehension
def matrix_add_concise(A, B):
    return [[A[i][j] + B[i][j] for j in range(len(A[0]))] for i in range(len(A))]

# Matrix multiplication
def matrix_multiply(A, B):
    # Check if matrices can be multiplied
    if len(A[0]) != len(B):
        raise ValueError("Number of columns in first matrix must equal number of rows in second matrix")
    
    result = []
    for i in range(len(A)):
        row = []
        for j in range(len(B[0])):
            # Calculate dot product of row i from A and column j from B
            element = sum(A[i][k] * B[k][j] for k in range(len(B)))
            row.append(element)
        result.append(row)
    return result

# Example usage
A = [[1, 2], [3, 4]]
B = [[5, 6], [7, 8]]

print(matrix_add(A, B))         # [[6, 8], [10, 12]]
print(matrix_multiply(A, B))    # [[19, 22], [43, 50]]

Matrix Rotation

Rotating matrices is a common operation in image processing and grid-based games.

# Rotate a matrix 90 degrees clockwise
def rotate_90_clockwise(matrix):
    # First transpose, then reverse each row
    transposed = [[matrix[j][i] for j in range(len(matrix))] for i in range(len(matrix[0]))]
    return [row[::-1] for row in transposed]

# Rotate a matrix 90 degrees counter-clockwise
def rotate_90_counter_clockwise(matrix):
    # First transpose, then reverse each column
    transposed = [[matrix[j][i] for j in range(len(matrix))] for i in range(len(matrix[0]))]
    return [row for row in transposed][::-1]

# Rotate a matrix 180 degrees
def rotate_180(matrix):
    # Reverse rows, then reverse elements in each row
    return [row[::-1] for row in matrix][::-1]

# Example
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

print("Original:")
for row in matrix:
    print(row)

print("\nRotated 90° clockwise:")
rotated = rotate_90_clockwise(matrix)
for row in rotated:
    print(row)  # [[7, 4, 1], [8, 5, 2], [9, 6, 3]]

Searching in Multidimensional Lists

Finding elements or patterns in multidimensional structures is a common requirement.

# Linear search in a 2D list
def find_element(matrix, target):
    for i, row in enumerate(matrix):
        for j, element in enumerate(row):
            if element == target:
                return (i, j)  # Return coordinates if found
    return None  # Return None if not found

# Binary search in a sorted 2D matrix
def binary_search_2d(matrix, target):
    # This works for matrices sorted row-wise and column-wise
    # Start from top-right corner
    rows, cols = len(matrix), len(matrix[0])
    i, j = 0, cols - 1
    
    while i < rows and j >= 0:
        if matrix[i][j] == target:
            return (i, j)
        elif matrix[i][j] > target:
            j -= 1  # Move left
        else:
            i += 1  # Move down
    
    return None

# Example
matrix = [
    [1, 4, 7, 11],
    [2, 5, 8, 12],
    [3, 6, 9, 16],
    [10, 13, 14, 17]
]

print(find_element(matrix, 8))      # (1, 2)
print(binary_search_2d(matrix, 14)) # (3, 2)

NumPy for Multidimensional Arrays

While Python's built-in lists are flexible, they aren't optimized for large-scale numerical operations. NumPy (Numerical Python) provides a more efficient alternative with its ndarray (n-dimensional array) object.

Converting Between Lists and NumPy Arrays

You can easily convert between nested lists and NumPy arrays.

import numpy as np

# Converting from lists to NumPy arrays
matrix_list = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]
matrix_np = np.array(matrix_list)
print(matrix_np)
# Output:
# [[1 2 3]
#  [4 5 6]
#  [7 8 9]]

# Converting back to nested lists
list_again = matrix_np.tolist()
print(list_again)  # [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

NumPy Array Advantages

NumPy arrays offer several advantages over nested lists for multidimensional data.

# Efficient creation of arrays
zeros = np.zeros((3, 4))       # 3x4 array of zeros
ones = np.ones((2, 2, 2))      # 2x2x2 array of ones
identity = np.eye(3)           # 3x3 identity matrix
range_arr = np.arange(9).reshape(3, 3)  # Reshape into 3x3

# Efficient slicing operations
arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(arr[:, 1])   # Second column: [2, 5, 8]
print(arr[1, :])   # Second row: [4, 5, 6]
print(arr[0:2, 1:3])  # 2x2 submatrix: [[2, 3], [5, 6]]

# Vectorized operations (much faster than loops)
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
print(a + b)       # Element-wise addition
print(a * b)       # Element-wise multiplication
print(np.dot(a, b))  # Matrix multiplication

# Statistical operations
data = np.array([[1, 2, 3], [4, 5, 6]])
print(np.mean(data))          # Overall mean: 3.5
print(np.mean(data, axis=0))  # Column means: [2.5, 3.5, 4.5]
print(np.mean(data, axis=1))  # Row means: [2.0, 5.0]
print(np.std(data))           # Standard deviation

# Reshaping and transposing
arr = np.arange(12)
reshaped = arr.reshape(3, 4)  # Reshape to 3x4
print(reshaped.T)             # Transpose (4x3 now)

# Broadcasting (automatic size matching)
grid = np.array([[1, 2, 3], [4, 5, 6]])
print(grid * 2)               # Multiply every element by 2
print(grid + np.array([10, 20, 30]))  # Add to each row

When to Use NumPy vs. Nested Lists

Feature Nested Lists NumPy Arrays
Performance Slower for numerical operations Optimized for numerical operations
Memory usage Higher memory overhead More compact memory usage
Element types Can mix types Homogeneous (same type)
Shape flexibility Can have irregular shapes Requires consistent dimensions
Built-in methods Basic list methods Extensive mathematical functions

Best Practices and Common Pitfalls

Memory Management

Properly managing memory is crucial when working with large multidimensional structures.

# Shallow vs. Deep copying
import copy

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

# Direct assignment (creates a reference, not a copy)
reference = original  # Both variables point to the same list
reference[0][0] = 999
print(original)  # [[999, 2, 3], [4, 5, 6]] - original is also changed

# Shallow copy (copies the outer list but not the inner ones)
original = [[1, 2, 3], [4, 5, 6]]
shallow = original.copy()  # or list(original) or original[:]
shallow[0][0] = 999
print(original)  # [[999, 2, 3], [4, 5, 6]] - inner lists are still shared

# Deep copy (fully independent copy)
original = [[1, 2, 3], [4, 5, 6]]
deep = copy.deepcopy(original)
deep[0][0] = 999
print(original)  # [[1, 2, 3], [4, 5, 6]] - original unchanged

Avoiding Common Pitfalls

Here are some common mistakes to avoid when working with multidimensional lists.

# Pitfall 1: Creating with multiplication (creates references)
wrong = [[0] * 3] * 3  # Creates 3 references to the same inner list
wrong[0][0] = 1
print(wrong)  # [[1, 0, 0], [1, 0, 0], [1, 0, 0]] - all rows changed

# Pitfall 2: Assuming all rows have the same length
jagged = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
# This will fail:
# for i in range(len(jagged)):
#     for j in range(len(jagged[0])):  # Assuming all rows match length of first row
#         print(jagged[i][j])  # IndexError when i=1, j=2

# Safer approach:
for row in jagged:
    for element in row:
        print(element, end=" ")
    print()

# Pitfall 3: Not checking bounds when accessing elements
matrix = [[1, 2], [3, 4]]

# Safe access:
def safe_access(matrix, i, j):
    if 0 <= i < len(matrix) and 0 <= j < len(matrix[i]):
        return matrix[i][j]
    return None  # Or raise custom exception

# Pitfall 4: Modifying lists during iteration
# DON'T do this:
# for row in matrix:
#     row.append(0)  # Modifies while iterating

# Instead, create a new structure:
new_matrix = [row + [0] for row in matrix]

Performance Optimization

Tips for improving performance when working with large multidimensional structures.

# Tip 1: Use list comprehensions instead of nested loops when possible
# Slower:
result = []
for i in range(5):
    row = []
    for j in range(5):
        row.append(i * j)
    result.append(row)

# Faster:
result = [[i * j for j in range(5)] for i in range(5)]

# Tip 2: Pre-allocate memory when you know the size
n = 100
# Slower (grows list dynamically):
matrix = []
for i in range(n):
    row = []
    for j in range(n):
        row.append(0)
    matrix.append(row)

# Faster (allocates once):
matrix = [[0 for _ in range(n)] for _ in range(n)]

# Tip 3: Use NumPy for large numerical computations
import numpy as np
import time

# Example operation: Multiply all elements by 2
size = 1000
matrix = [[i+j for j in range(size)] for i in range(size)]
np_matrix = np.array(matrix)

# Pure Python approach
start = time.time()
result1 = [[cell * 2 for cell in row] for row in matrix]
python_time = time.time() - start

# NumPy approach
start = time.time()
result2 = np_matrix * 2
numpy_time = time.time() - start

print(f"Python time: {python_time:.6f} sec")
print(f"NumPy time: {numpy_time:.6f} sec")
print(f"NumPy is {python_time/numpy_time:.1f}x faster")

Real-world Applications

Multidimensional lists are used in a variety of applications. Here are some practical examples:

Game Development: Conway's Game of Life

A simple implementation of the classic cellular automaton that uses a 2D grid.

def next_generation(grid):
    rows, cols = len(grid), len(grid[0])
    new_grid = [[0 for _ in range(cols)] for _ in range(rows)]
    
    # Calculate the next generation
    for i in range(rows):
        for j in range(cols):
            # Count live neighbors
            live_neighbors = 0
            for ni in range(max(0, i-1), min(rows, i+2)):
                for nj in range(max(0, j-1), min(cols, j+2)):
                    if (ni, nj) != (i, j) and grid[ni][nj] == 1:
                        live_neighbors += 1
            
            # Apply Game of Life rules
            if grid[i][j] == 1:  # Cell is alive
                if live_neighbors < 2 or live_neighbors > 3:
                    new_grid[i][j] = 0  # Dies (underpopulation or overpopulation)
                else:
                    new_grid[i][j] = 1  # Survives
            else:  # Cell is dead
                if live_neighbors == 3:
                    new_grid[i][j] = 1  # Becomes alive (reproduction)
    
    return new_grid

# Example: Glider pattern
grid = [
    [0, 0, 0, 0, 0],
    [0, 0, 1, 0, 0],
    [0, 0, 0, 1, 0],
    [0, 1, 1, 1, 0],
    [0, 0, 0, 0, 0]
]

# Visualize the grid
def print_grid(grid):
    for row in grid:
        print(''.join('■' if cell else '□' for cell in row))
    print()

# Run simulation for a few generations
print("Initial state:")
print_grid(grid)

for i in range(4):
    grid = next_generation(grid)
    print(f"Generation {i+1}:")
    print_grid(grid)

Image Processing: Simple Edge Detection

Using a 2D convolution with a Sobel operator to detect edges in an image.

# Import required libraries
import numpy as np
from PIL import Image

def simple_edge_detection(image_array):
    # Sobel operators for edge detection
    sobel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
    sobel_y = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]])
    
    height, width = image_array.shape
    edges = np.zeros((height-2, width-2))
    
    # Apply convolution
    for i in range(1, height-1):
        for j in range(1, width-1):
            # Get 3x3 neighborhood
            neighborhood = image_array[i-1:i+2, j-1:j+2]
            
            # Calculate gradients
            gx = np.sum(neighborhood * sobel_x)
            gy = np.sum(neighborhood * sobel_y)
            
            # Calculate gradient magnitude
            edges[i-1][j-1] = min(255, np.sqrt(gx**2 + gy**2))
    
    return edges

# Example usage (pseudo-code - requires PIL and an image file)
# image = Image.open('image.jpg').convert('L')  # Convert to grayscale
# image_array = np.array(image)
# edges = simple_edge_detection(image_array)
# edge_image = Image.fromarray(edges.astype(np.uint8))
# edge_image.save('edges.jpg')

Practice Exercises

Try These:

  1. Create a function that finds the largest element in a 2D list and returns its position (row, column).
  2. Write a function to check if a given matrix is symmetric (equal to its transpose).
  3. Implement a function that rotates a square matrix 90 degrees counter-clockwise in-place.
  4. Create a function that performs matrix multiplication for two matrices of any compatible dimensions.
  5. Write a program that implements the classic "snake" game using a 2D grid to represent the game board.
  6. Create a function that checks if a given Sudoku solution is valid.
  7. Implement a flood-fill algorithm on a 2D grid (similar to the "paint bucket" tool in image editors).
  8. Write a function that converts between a 2D grid and a 1D array, maintaining the correct mapping.