Extended slicing with custom steps. List comprehensions with multiple conditions. The bisect module for maintaining sorted lists. array for memory-efficient numeric storage. deque for fast appends and pops from both ends.
This lesson explores these advanced list techniques. You will learn to slice lists with custom steps, use nested comprehensions, maintain sorted lists with binary search, and choose the right sequence type for your needs.
🕯️ Magic Note
The list type in Python is implemented as a dynamic array. Appending to the end is O(1) amortized, but inserting or deleting at the beginning is O(n). For frequent insertions at both ends, use collections.deque instead.
Python
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# Basic slicing [start:stop:step]
print(numbers[::2]) # [0, 2, 4, 6, 8] (every second)
print(numbers[1::2]) # [1, 3, 5, 7, 9] (every second starting at 1)
print(numbers[2:8:2]) # [2, 4, 6]
# Negative step (reverses order)
print(numbers[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] (reverse)
print(numbers[5:1:-1]) # [5, 4, 3, 2]
# Modifying slices with step (must match length)
numbers[::2] = [0, 0, 0, 0, 0]
print(numbers) # [0, 1, 0, 3, 0, 5, 0, 7, 0, 9]
# Deleting slices with step
del numbers[1::2]
print(numbers) # [0, 0, 0, 0, 0]
Python
# Flatten a matrix (nested for loops)
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [num for row in matrix for num in row]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Equivalent to:
flat = []
for row in matrix:
for num in row:
flat.append(num)
# 2D list comprehension (create matrix)
matrix = [[i * j for j in range(1, 4)] for i in range(1, 4)]
print(matrix) # [[1, 2, 3], [2, 4, 6], [3, 6, 9]]
# Nested comprehensions with condition
pairs = [(x, y) for x in range(3) for y in range(3) if x != y]
print(pairs) # [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]
# Cartesian product
colors = [“red”, “blue”]
sizes = [“S”, “M”, “L”]
products = [(c, s) for c in colors for s in sizes]
print(products) # [(‘red’, ‘S’), (‘red’, ‘M’), (‘red’, ‘L’), (‘blue’, ‘S’), (‘blue’, ‘M’), (‘blue’, ‘L’)]
🕯️ Magic Note
The order of for clauses in a nested comprehension follows the order of nesting. The outer loop comes first, then the inner loops. This matches the order you would write in a regular nested loop.
Python
# Single condition (filter)
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = [x for x in numbers if x % 2 == 0]
print(evens) # [2, 4, 6, 8, 10]
divisible = [x for x in range(1, 51) if x % 3 == 0 if x % 5 == 0]
print(divisible) # [15, 30, 45] (divisible by both 3 and 5)
# if-else in expression
labels = [“even” if x % 2 == 0 else “odd” for x in range(1, 6)]
print(labels) # [‘odd’, ‘even’, ‘odd’, ‘even’, ‘odd’]
# Using a function in comprehension
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
primes = [x for x in range(2, 50) if is_prime(x)]
print(primes) # [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
Python
import bisect
sorted_list = [1, 3, 5, 7, 9]
# Find insertion point (index where to insert to keep order)
pos = bisect.bisect_left(sorted_list, 5)
print(pos) # 2 (index where 5 would go)
# bisect_right / bisect (insert after existing equal elements)
pos = bisect.bisect_right(sorted_list, 5)
print(pos) # 3 (for duplicates, inserts after)
# Insert while maintaining order (insort)
bisect.insort(sorted_list, 4)
print(sorted_list) # [1, 3, 4, 5, 7, 9]
# insort_left / insort_right for duplicate handling
bisect.insort_left(sorted_list, 5)
print(sorted_list) # [1, 3, 4, 5, 5, 7, 9]
# Using bisect for binary search
def binary_search(lst, target):
i = bisect.bisect_left(lst, target)
if i != len(lst) and lst[i] == target:
return i
return -1
print(binary_search(sorted_list, 5)) # 3
print(binary_search(sorted_list, 10)) # -1
🕯️ Magic Note
bisect uses binary search with O(log n) time complexity, much faster than linear search for large lists. Use it when you need to maintain a sorted list and perform frequent searches or insertions.
Python
from collections import deque
# Create deque
dq = deque([1, 2, 3])
print(dq) # deque([1, 2, 3])
# Append to right (same as list append)
dq.append(4) # deque([1, 2, 3, 4])
# Append to left (O(1) vs list insert(0) which is O(n))
dq.appendleft(0) # deque([0, 1, 2, 3, 4])
# Pop from right
last = dq.pop() # returns 4, deque([0, 1, 2, 3])
# Pop from left (O(1) vs list pop(0) which is O(n))
first = dq.popleft() # returns 0, deque([1, 2, 3])
# Extend from iterable
dq.extend([4, 5, 6]) # deque([1, 2, 3, 4, 5, 6])
dq.extendleft([-2, -1, 0]) # deque([0, -1, -2, 1, 2, 3, 4, 5, 6]) (note order!)
# Rotate deque
dq = deque([1, 2, 3, 4, 5])
dq.rotate(2) # deque([4, 5, 1, 2, 3]) (rotate right)
dq.rotate(-1) # deque([5, 1, 2, 3, 4]) (rotate left)
# Max length (fixed-size queue)
bounded = deque(maxlen=3)
for i in range(5):
bounded.append(i)
print(bounded) # oldest elements are dropped
Python
from array import array
import sys
# Type codes: ‘i’ (signed int), ‘f’ (float), ‘d’ (double), ‘b’ (signed char)
arr = array(“i”, [1, 2, 3, 4, 5]) # Array of signed integers
print(arr) # array(‘i’, [1, 2, 3, 4, 5])
# Array vs list memory usage
lst = list(range(1000000))
arr = array(“i”, range(1000000))
print(f”List memory: {sys.getsizeof(lst) / 1024 / 1024:.2f} MB”)
print(f”Array memory: {sys.getsizeof(arr) / 1024 / 1024:.2f} MB”)
# Array methods similar to lists
arr.append(6)
arr.extend([7, 8, 9])
arr.insert(0, 0)
print(arr[:5]) # array(‘i’, [0, 1, 2, 3, 4])
# Type codes
print(“Type codes: ‘b’ (signed char), ‘B’ (unsigned char), ‘i’ (signed int), ‘I’ (unsigned int)”)
print(” ‘l’ (signed long), ‘L’ (unsigned long), ‘q’ (long long), ‘Q’ (unsigned long long)”)
print(” ‘f’ (float), ‘d’ (double)”)
🕯️ Magic Note
The array module is more memory-efficient than lists for numeric data because it stores values in contiguous memory (like C arrays). Use it when working with large numeric datasets that fit in memory but where you want to save space.
Python
import itertools
# chain: combine multiple iterables
result = list(itertools.chain([1, 2], [3, 4], [5, 6]))
print(result) # [1, 2, 3, 4, 5, 6]
# islice: slice iterator (lazy)
infinite = itertools.count()
first_ten = list(itertools.islice(infinite, 10))
print(first_ten) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# tee: create multiple independent iterators
original = [1, 2, 3, 4, 5]
iter1, iter2 = itertools.tee(original, 2)
print(list(iter1)) # [1, 2, 3, 4, 5]
print(list(iter2)) # [1, 2, 3, 4, 5]
# zip_longest: zip with fill value
result = list(itertools.zip_longest([1, 2], [3, 4, 5], fillvalue=0))
print(result) # [(1, 3), (2, 4), (0, 5)]
# permutations, combinations
items = [“A”, “B”, “C”]
perms = list(itertools.permutations(items, 2))
print(perms) # [(‘A’, ‘B’), (‘A’, ‘C’), (‘B’, ‘A’), (‘B’, ‘C’), (‘C’, ‘A’), (‘C’, ‘B’)]
combs = list(itertools.combinations(items, 2))
print(combs) # [(‘A’, ‘B’), (‘A’, ‘C’), (‘B’, ‘C’)]
Python
from collections import deque
from bisect import insort
class TaskQueue:
def __init__(self):
self._high_priority = deque()
self._normal_priority = deque()
self._scheduled = [] # Sorted list for scheduled tasks
def add_high_priority(self, task):
self._high_priority.append(task)
def add_normal_priority(self, task):
self._normal_priority.append(task)
def add_scheduled(self, execute_time, task):
insort(self._scheduled, (execute_time, task))
def get_next(self, current_time=None):
if self._high_priority:
return self._high_priority.popleft()
if self._scheduled and current_time:
if self._scheduled[0][0] <= current_time:
return self._scheduled.pop(0)[1]
if self._normal_priority:
return self._normal_priority.popleft()
return None
queue = TaskQueue()
queue.add_high_priority(“Urgent task”)
queue.add_normal_priority(“Normal task 1”)
queue.add_normal_priority(“Normal task 2”)
print(queue.get_next()) # Urgent task
print(queue.get_next()) # Normal task 1
print(queue.get_next()) # Normal task 2
- Modifying a list while iterating over it (use a copy or iterate backwards)
- Using list for frequent insertions at the beginning (use deque)
- Forgetting that extended slice assignment requires the right number of elements
- Using nested comprehensions that are too complex (break them into steps)
- Assuming list slicing creates a view (it creates a new list)
- Using bisect on unsorted lists (results are unpredictable)
- Write a list comprehension that creates a list of squares for numbers 1 to 20 that are divisible by 3.
- How do you reverse a list using slicing?
- What is the difference between list.append() and deque.appendleft()?
- Write a function that maintains a sorted list using bisect.insort.
- Flatten a nested list [[1,2],[3,4],[5,6]] using a list comprehension.
- When would you use array.array instead of a list?
⚡ Whisper
Lists are the containers you reach for first. But advanced lists are tools you reach for next. Slicing with step extracts every nth item. Nested comprehensions flatten matrices. bisect keeps your lists sorted without sorting. deque makes both ends fast. array saves memory for numbers.itertools chains, slices, and combines. Each tool solves a problem the basic list cannot. Choose the right one. Your code will be faster, clearer, and more efficient. The list is not just a list. It is a gateway to sequence mastery. Walk through it.