This chapter is a focused crash course on the Python features that come up repeatedly in DSA and preparing for coding interviews. Instead of covering the entire language, we will concentrate only on the parts that matter for solving problems efficiently in interviews.
On LeetCode, most imports are available automatically. But when writing Python locally or on some interview platforms, you need to know what to import. Here are the imports that cover 95% of DSA problems:
Each of these modules serves a specific purpose in DSA:
| Module | What It Provides | DSA Use Case |
|---|---|---|
collections | defaultdict, Counter, deque, OrderedDict | Frequency counting, BFS queues, adjacency lists |
heapq | Min-heap operations | Top-K elements, Dijkstra, merge-K-sorted |
bisect | Binary search on sorted lists | Insertion point, sorted container operations |
functools | lru_cache, cache, cmp_to_key | DP memoization, custom sorting |
itertools | combinations, permutations, product, accumulate | Generating subsets, prefix sums |
math | inf, gcd, isqrt, ceil, floor | Sentinel values, number theory |
typing | Type hints | Code clarity on LeetCode |
sys | setrecursionlimit | Deep recursion for DFS/trees |
One line is worth adding up front:
Python's default recursion limit is 1000. Without this line, DFS on a graph with 100,000 nodes or a skewed binary tree raises a RecursionError. We will cover recursion in detail later.
There is also one third-party library available on LeetCode that provides sorted container data structures:
This is not part of Python's standard library, but it is available on LeetCode and many interview platforms. We will cover it in the Sorted Containers section.
Python is a dynamically typed language. You do not declare types, you just assign values. The interpreter figures out the type at runtime:
For DSA, a small set of types covers most problems:
| Type | Example | DSA Use Case |
|---|---|---|
int | 42, 2**100 | Array indices, counters, any integer (no overflow!) |
float | 3.14, float('inf') | Sentinel values, rarely used otherwise |
bool | True, False | Visited arrays, flags |
str | "hello" | Immutable character sequences |
None | None | Null equivalent, tree/linked list terminators |
Python integers have arbitrary precision, which is a big advantage for DSA. Python integers never overflow.
This means you never need to worry about integer overflow in most DSA problems. The safe binary search midpoint formula left + (right - left) // 2 is still good practice for clarity and habit, but technically unnecessary in Python because (left + right) // 2 will never overflow.
This distinction matters:
That last line is important. In Python, -7 // 2 gives -4 because floor division rounds toward negative infinity, not toward zero. If you need truncation toward zero, use int(-7 / 2) or math.trunc(-7 / 2).
Python writes conditional expressions inline using if/else:
Python supports multiple assignment, which is handy for DSA:
The swap trick is particularly useful in partition-based algorithms and two-pointer problems where you constantly need to swap elements.
Python supports optional type hints that make your code clearer on LeetCode:
Type hints have no runtime effect. They are purely for readability. LeetCode uses them in function signatures, so they appear throughout this course.
A quirk worth knowing: bool is a subclass of int in Python. True is 1 and False is 0:
This is useful for counting conditions:
Most operators in Python work as you would expect, but a few deserve special attention for DSA.
The % operator in Python always returns a result with the same sign as the divisor (the right operand). This is convenient for DSA:
This means you do not need the ((n % m) + m) % m trick to force a positive modulo result.
Many problems ask you to return the result "modulo 10^9 + 7":
Bitwise operations come up in several DSA patterns:
Key bitwise operators:
| Operator | Symbol | DSA Use |
|---|---|---|
| AND | & | Masking, checking if bit is set: (n & (1 << i)) != 0 |
| OR | | | Setting bits: n | (1 << i) |
| XOR | ^ | Finding unique elements, toggling bits |
| NOT | ~ | Bit inversion (~n equals -(n+1) in Python) |
| Left shift | << | Multiply by powers of 2: 1 << n equals 2^n |
| Right shift | >> | Divide by powers of 2 |
A common bit manipulation pattern is checking and setting individual bits, which shows up in problems using bitmasks to represent subsets:
Python also provides helpful built-in functions for bit operations:
Python does not have an unsigned right shift operator. Since Python integers have arbitrary precision, there is no fixed bit width, so unsigned shift does not apply. For problems that specifically require unsigned right shift behavior (like reversing bits of a 32-bit integer), you need to mask with & 0xFFFFFFFF to simulate 32-bit unsigned behavior.
Python offers several loop forms. Use each where it fits:
Use enumerate() for index-value pairs. When you need both the index and the value, use enumerate() instead of for i in range(len(nums)). It is cleaner and less error-prone:
Python uses the keywords and and or for boolean logic:
Python's or has a useful idiom for default values:
Be careful with this pattern when 0 or "" are valid values. In those cases, use x if x is not None else default.
:=Introduced in Python 3.8, the walrus operator assigns and returns a value in a single expression. It can make some DSA patterns more concise:
You do not need to use the walrus operator in interviews, but knowing it exists helps you read others' solutions.
In DSA problems, extracting logic into helper functions keeps the code clean. On LeetCode, your solution lives inside a class:
You can also define nested functions, which is common for DFS/backtracking:
Nested functions can access variables from the enclosing scope (like results and nums above). This is called a closure and it is convenient for DSA. It avoids passing extra parameters through recursive calls.
Python passes everything by object reference. This means:
This distinction matters in recursive and backtracking problems. When you pass a list to a recursive call and add elements to it, those additions are visible to the caller. That is why the backtracking pattern works, adding and removing from a shared list as you explore different branches:
The path[:] when saving results creates a copy. If you wrote results.append(path) instead, every entry in results would be a reference to the same list, and they would all end up empty after backtracking unwinds. This is a common bug in backtracking solutions.
This is a Python-specific trap to watch for:
If you need a nested function to modify an integer from the enclosing scope, you need the nonlocal keyword:
Without nonlocal, count += 1 would create a local variable instead of modifying the outer one. Alternatively, you can use a mutable container like a list [0] to avoid nonlocal, but that is less readable.
Lists are the foundational data structure in Python and the starting point for almost every DSA problem.
Watch for this pattern:
The [[0] * cols] * rows pattern is a Python-specific gotcha. Each row is a reference to the same list, so modifying one row modifies all of them. Always use a list comprehension for 2D arrays. This bug indicates a misunderstanding of Python's reference semantics.
| Operation | Syntax | Time | DSA Use |
|---|---|---|---|
| Append | lst.append(x) | O(1) amortized | Building result lists |
| Pop last | lst.pop() | O(1) | Stack operations |
| Pop at index | lst.pop(i) | O(n) | Avoid in tight loops |
| Insert at index | lst.insert(i, x) | O(n) | Avoid in tight loops |
| Access | lst[i] | O(1) | Random access |
| Update | lst[i] = x | O(1) | In-place modification |
| Length | len(lst) | O(1) | Loop bounds |
| Contains | x in lst | O(n) | Use set for O(1) |
| Reverse in-place | lst.reverse() | O(n) | Reverse array |
| Reversed copy | lst[::-1] | O(n) | New reversed list |
| Sort in-place | lst.sort() | O(n log n) | Sorting |
| Sorted copy | sorted(lst) | O(n log n) | New sorted list |
| Extend | lst.extend(other) | O(k) | Concatenate lists |
| Count | lst.count(x) | O(n) | Count occurrences |
| Index | lst.index(x) | O(n) | Find first occurrence |
| Clear | lst.clear() | O(1) | Reset list |
Python's negative indexing is one of its most useful features for DSA:
This comes up constantly. Peeking at the top of a stack? stack[-1]. Getting the last character of a string? s[-1]. Accessing the last row of a matrix? matrix[-1].
Slicing creates a new list from a portion of an existing one. The syntax is lst[start:stop:step], where start is inclusive and stop is exclusive:
Slicing is particularly useful for:
copy = lst[:] or copy = list(lst)reversed_list = lst[::-1]subarray = lst[i:j]Be aware that slicing creates a new list, so it costs O(k) time and space where k is the slice size. Do not slice inside a tight loop if you can avoid it.
List comprehensions are one of Python's most useful features for writing concise DSA code:
2D arrays appear in every matrix problem (BFS on grid, DP tables, etc.):
For DP problems, you often need a table with one extra row and column for the base case:
The 4-directional neighbor pattern is common in matrix problems:
For 8-directional movement (including diagonals), add the four diagonal pairs: (-1,-1), (-1,1), (1,-1), (1,1).
Tuple unpacking with for dr, dc in directions and the chained comparison 0 <= nr < rows make grid traversal code compact and readable.
Strings in Python are immutable. Every time you "modify" a string, Python creates a new object. This has a performance implication for DSA:
| Method | Example | DSA Use |
|---|---|---|
s[i] | s[0] | Access character (no charAt needed) |
len(s) | len(s) | Length (built-in function, not method) |
s[a:b] | s[0:3] | Substring via slicing |
s.split() | s.split(" ") | Tokenize into list |
"sep".join(lst) | ",".join(lst) | Build string from list |
s.strip() | s.strip() | Remove leading/trailing whitespace |
s.isalnum() | s.isalnum() | Alphanumeric check (valid palindrome) |
s.isalpha() | s.isalpha() | Letters only |
s.isdigit() | s.isdigit() | Digits only |
s.lower() | s.lower() | Lowercase conversion |
s.upper() | s.upper() | Uppercase conversion |
s.startswith(p) | s.startswith("ab") | Prefix check |
s.endswith(p) | s.endswith("ab") | Suffix check |
s.find(sub) | s.find("ab") | Find substring (-1 if not found) |
s.count(sub) | s.count("a") | Count occurrences |
s.replace(old, new) | s.replace("a", "b") | Replace all occurrences (returns new string) |
sub in s | "ab" in "abc" | Substring check |
ord() and chr()Python does not have a separate char type. Characters are just strings of length 1. To do arithmetic on characters (which is common in DSA), use ord() and chr():
The ord(c) - ord('a') pattern works because characters have numeric values. Subtracting ord('a') from a lowercase letter gives its zero-based position. This is how you build frequency arrays without a dictionary:
This is faster and more memory-efficient than Counter(s) when you know the character set is limited to lowercase (or uppercase, or digits). However, for most interview problems, Counter is perfectly acceptable and more readable.
Python's == operator compares string content, not references:
Use == for string equality. The is operator checks object identity, which is rarely what you want for strings.
Python's built-in dictionaries and sets, combined with the collections module, provide the data structures used in nearly every DSA problem. Choosing the right collection is often the difference between an O(n) and an O(n^2) solution.
dict is Python's built-in hash map. It provides O(1) average-case lookups, inserts, and deletes. Since Python 3.7, dictionaries maintain insertion order.
Key operations:
A safety point: d[key] raises KeyError if the key does not exist. Always use d.get(key, default) for safe access, or check key in d first. Forgetting this guard is a common source of bugs in interview code.
defaultdict from the collections module automatically creates a default value when you access a missing key. This eliminates the "check if key exists, create if not" pattern:
Compare this to the verbose alternative:
Common default factories:
| Factory | Default Value | Use Case |
|---|---|---|
int | 0 | Frequency counting |
list | [] | Adjacency lists, grouping |
set | set() | Unique neighbors, deduplication |
lambda: float('inf') | inf | Distance maps in shortest path |
Counter from collections is the most concise way to count frequencies:
Counter arithmetic is useful for DSA:
set provides O(1) average-case membership testing:
Unlike some languages, set.add() returns nothing, so you detect duplicates by checking membership before adding:
Set operations are useful for certain DSA problems:
Since Python 3.7, regular dict maintains insertion order. OrderedDict from collections is still useful for one specific operation: move_to_end(), which is used in LRU cache implementations:
Hashability matters for Python DSA. Only hashable objects can be dict keys or set elements. The rule is simple: mutable objects are not hashable, immutable objects are.
| Type | Hashable? | Can Be Dict Key / Set Element? |
|---|---|---|
int, float, bool | Yes | Yes |
str | Yes | Yes |
tuple (of hashable elements) | Yes | Yes |
frozenset | Yes | Yes |
list | No | No |
dict | No | No |
set | No | No |
This comes up constantly in DSA:
| Collection | Python Type | Key Methods | Time Complexity | DSA Use Case |
|---|---|---|---|---|
| Dynamic array | list | append, pop, [], len | O(1) append/access | Result lists, stacks |
| Hash map | dict | [], get, in, items | O(1) average | Frequency counting, lookups |
| Hash set | set | add, in, discard | O(1) average | Visited tracking, duplicates |
| Auto-default map | defaultdict | Same as dict + auto-default | O(1) average | Adjacency lists, grouping |
| Frequency map | Counter | most_common, arithmetic | O(1) average | Anagram, window problems |
| Ordered map | OrderedDict | move_to_end, popitem | O(1) average | LRU cache |
| Double-ended queue | deque | append, popleft, appendleft | O(1) both ends | BFS, sliding window |
| Min-heap | heapq on list | heappush, heappop | O(log n) push/pop | Top-K, Dijkstra |
| Sorted list | SortedList | add, remove, bisect | O(log n) | Sliding window sorted access |
| Immutable sequence | tuple | [], in, unpacking | O(1) access | Dict keys, heap elements, states |
| Immutable set | frozenset | Same as set (read-only) | O(1) lookup | Set of sets, dict keys |
Python tuples are immutable, fixed-size sequences. They are the standard way to group a small number of related values, and they simplify many DSA patterns: coordinates, edges, intervals, and heap entries.
1. Tuples are hashable (unlike lists), so they can be dict keys and set elements:
2. Tuples compare lexicographically, which is useful for heap operations:
This means you can push tuples onto a heap and they will be ordered by the first element, with ties broken by the second element, and so on:
3. Tuple unpacking makes code cleaner:
deque (double-ended queue) from collections provides O(1) operations at both ends. Use it for BFS, sliding window problems, and monotonic deque patterns.
Every BFS implementation starts with a deque:
For level-order BFS (where you need to process all nodes at the current level before moving to the next), capture the queue size:
The monotonic deque is used in "sliding window maximum/minimum" problems:
You can create a deque with a maximum size. When full, adding to one end automatically removes from the other:
Python's heapq module provides heap operations on a regular list. The heap is always a min-heap, meaning the smallest element is always at index 0.
Python only provides a min-heap. To get a max-heap, negate the values:
This works because negating reverses the ordering: if a < b, then -a > -b.
Since tuples compare lexicographically, you can push tuples to sort by multiple fields. The first element of the tuple determines the primary sort order:
Caution with tuples: If the first elements are equal, Python compares the second elements. If the second elements are not comparable (e.g., custom objects without __lt__), you get a TypeError. Always include a tie-breaking field (like an index or counter) as the second element:
One important limitation: heapq does not support efficient removal of arbitrary elements. Removing a specific element that is not at the root requires an O(n) scan, and there is no remove method. If you need to invalidate heap elements, use lazy deletion: mark elements as deleted and skip them when they surface via heappop(). Alternatively, use SortedList from sortedcontainers which supports O(log n) removal.
Python does not ship a balanced binary search tree in its standard library, but it offers two ways to work with sorted data: the bisect module for binary search on sorted lists, and the third-party sortedcontainers library (available on LeetCode) for O(log n) sorted maps and sets.
The bisect module performs binary search to find insertion points in a sorted list:
The difference between bisect_left and bisect_right matters when the value exists in the list:
bisect_left(lst, x): returns the index of the leftmost position where x can be inserted (i.e., the index of the first element >= x)bisect_right(lst, x): returns the index of the rightmost position where x can be inserted (i.e., the index of the first element > x)This makes bisect_left useful for "find the first element >= target" and bisect_right for "find the first element > target":
Insert while maintaining sort order:
Note that insort is O(log n) for the search but O(n) for the insertion (because shifting elements in a list is O(n)). For large lists with frequent insertions, use SortedList instead.
The sortedcontainers library provides three O(log n) sorted data structures: SortedList, SortedDict, and SortedSet.
Common neighbor queries on a SortedList:
| Query | Expression | Notes |
|---|---|---|
| Floor (largest <= x) | sl[sl.bisect_right(x) - 1] | Check index >= 0 first |
| Ceiling (smallest >= x) | sl[sl.bisect_left(x)] | Check index < len(sl) first |
| Strictly lower (largest < x) | sl[sl.bisect_left(x) - 1] | Check index >= 0 first |
| Strictly higher (smallest > x) | sl[sl.bisect_right(x)] | Check index < len(sl) first |
| Smallest element | sl[0] | O(1) access |
| Largest element | sl[-1] | O(1) access |
Sorting is a prerequisite for many algorithms: binary search, two pointers on sorted arrays, merge intervals, and greedy approaches.
The key parameter takes a function that extracts a comparison key from each element. This is how you sort by custom criteria:
The tuple trick handles multi-key sorting cleanly. Python compares tuples lexicographically, so (len(w), w) sorts by length first, then alphabetically for words of the same length.
To sort one key ascending and another descending, negate the ascending key (for numbers) or reverse the sort:
For rare problems where the comparison logic cannot be expressed as a simple key extraction, use cmp_to_key:
Python's sort is stable (it uses TimSort). Elements that compare equal retain their relative order from the original list. This is useful for multi-key sorting. You can sort by secondary key first, then by primary key, and the secondary order is preserved within groups of equal primary keys:
Python offers several patterns that make DSA code more concise and readable. They reduce the number of lines you write in an interview, leaving more time for the actual algorithm.
List comprehensions create lists in a single expression:
Generator expressions are like list comprehensions but lazy. They do not create the entire list in memory:
zip() pairs up elements from multiple iterables:
reversed() returns an iterator that traverses in reverse. Unlike lst[::-1], it does not create a new list:
This pattern is useful for sliding window problems:
While list comprehensions are generally preferred, map() can be useful for type conversions:
The @cache and @lru_cache decorators turn any recursive function into a memoized solution with zero boilerplate. Many DP problems that would normally require a manual memo dictionary or a full bottom-up table can be solved with a decorated recursive function.
@cache is shorthand for @lru_cache(maxsize=None), introduced in Python 3.9. Both cache all results indefinitely.
On LeetCode, your solution is a class method. Here is how to use @cache inside a class:
The nested function approach works cleanly because coins is captured from the enclosing scope and does not need to be a parameter (which would affect the cache key).
Arguments must be hashable. You cannot pass lists to a cached function. Convert them to tuples:
Remember to clear the cache if you run multiple test cases and the cached function uses external state. On LeetCode, this is rarely needed because each test case creates a new Solution instance. But in local testing:
Sometimes @cache is not the best fit:
Use manual memo when:
Every function call in Python goes on the call stack. Python's default recursion limit is 1000, which is extremely low for DSA problems:
The fix is one line:
| Scenario | Typical Depth | Risk |
|---|---|---|
| Balanced binary tree (n nodes) | O(log n) | Safe for n up to 10^6 |
| Linked list / skewed tree (n nodes) | O(n) | Dangerous if n > 1000 (default limit) |
| Backtracking (k choices, depth d) | O(d) | Usually safe (d is small) |
| DFS on graph (n nodes) | O(n) | Set recursionlimit for large n |
When recursion depth is proportional to input size and the input can be large, convert to iteration using an explicit stack:
Note that Python does not optimize tail recursion. Unlike some functional languages, a tail-recursive function in Python still adds a frame to the call stack on every call.
Graphs appear in a large portion of DSA problems. Python does not have a built-in graph class, so you need to build representations yourself. There are two common approaches.
Use when node IDs can be anything (integers, strings, etc.):
More memory-efficient when node IDs are contiguous integers:
Store tuples of (neighbor, weight):
| Approach | Pros | Cons | Use When |
|---|---|---|---|
defaultdict(list) | Handles any node IDs, no wasted space | Slightly slower due to hashing | Node IDs are large, sparse, or non-numeric |
[[] for _ in range(n)] | Fast index access, no hashing overhead | Wastes space if node IDs are sparse | Nodes are 0 to n-1 |
Modifying a dictionary or set while iterating over it causes a RuntimeError. Modifying a list during iteration does not raise an error, but it causes silent bugs as indices shift underneath you.
The same applies to sets:
Modifying a list while iterating does not raise an error, but it causes subtle bugs because indices shift:
In practice, most DSA problems do not require removing during iteration. You are far more likely to build a new collection with the desired elements.
Many DSA problems require avoiding duplicate results (e.g., 3Sum, 4Sum, permutations with duplicates). Python offers two approaches.
This is the preferred approach for sorted array problems:
This runs in O(1) extra space (beyond the sort) and produces results in sorted order.
Use a set to collect unique results. Remember that lists are not hashable, so convert to tuples:
The sort-and-skip approach is generally preferred in interviews because it avoids the overhead of hashing and produces cleaner code. But set-based deduplication is simpler to implement and is a valid fallback when the sort-and-skip logic is complex.
This section collects the small patterns and utilities that come up repeatedly across many problem types.
Many DSA problems use custom node classes. LeetCode typically defines these for you, but you should know the patterns:
If you need to put custom objects in a heap, define __lt__ (less than):
Alternatively, use a tuple with a counter as a tie-breaker (as shown in the heapq section). The tuple approach is generally preferred because it does not require modifying the class definition.
Python is one of the slower languages used in competitive programming. LeetCode adjusts time limits per language, so this rarely matters in practice, but understanding common performance pitfalls helps you avoid unnecessary TLE (Time Limit Exceeded) verdicts.
| Operation | Slow | Fast | Why |
|---|---|---|---|
| BFS queue | list.pop(0) | deque.popleft() | O(n) vs O(1) |
| String building | s += char in loop | "".join(parts) | O(n^2) vs O(n) |
| Membership test | x in list | x in set | O(n) vs O(1) |
| Insert at front | list.insert(0, x) | deque.appendleft(x) | O(n) vs O(1) |
| Sorted insert | insort(list, x) | SortedList.add(x) | O(n) vs O(log n) |
| Deep copy | copy.deepcopy() | [row[:] for row in matrix] | General vs specific |
set for lookups, deque for BFS, heapq for priority queues.@cache for memoization. It is faster than a manual memo dictionary for most cases.append in a loop. They avoid the repeated attribute lookup of .append and use optimized bytecode.str += char in loops. Use "".join().sys.setrecursionlimit for deep recursion rather than converting to iteration (unless the depth is truly massive).