Last Updated: March 31, 2026
This chapter is a focused crash course on the Python features you will use repeatedly while learning 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.
Before we get into the language itself, let us address something practical. 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 import deserves special mention right away:
Python's default recursion limit is 1000. For DFS on a graph with 100,000 nodes or a skewed binary tree, you will hit a RecursionError without this line. We will cover recursion in detail later, but keep this in the back of your mind.
There is also one third-party library available on LeetCode that acts as Python's equivalent of Java's TreeMap and TreeSet:
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, you will use a small set of types over and over:
| 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 |
Now here is the single biggest advantage Python has over Java and C++ for DSA: Python integers have arbitrary precision. There is no overflow. Ever.
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 is a critical distinction that catches many candidates:
That last line is important. In Java, -7 / 2 gives -3 (truncates toward zero). In Python, -7 // 2 gives -4 (floors toward negative infinity). If you need truncation toward zero like Java, use int(-7 / 2) or math.trunc(-7 / 2).
Python's equivalent of Java's condition ? valueIfTrue : valueIfFalse:
One of Python's most elegant features 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 you will see them 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 actually more useful for DSA than Java's behavior:
This means you do not need the ((n % m) + m) % m trick that Java requires for positive modulo results.
Many problems ask you to return the result "modulo 10^9 + 7":
Bitwise operations unlock an entire category of problems:
The key bitwise operators you will encounter:
| 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 (>>> in Java). 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:
`enumerate()` is your best friend. Whenever you find yourself writing for i in range(len(nums)) just to access both the index and value, use enumerate() instead. It is cleaner, more Pythonic, and less error-prone:
Python uses and and or (not && and || like Java):
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, you will frequently extract logic into helper functions. 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 incredibly convenient for DSA. It avoids passing extra parameters through recursive calls.
A critical concept to understand: Python passes everything by object reference. This means:
This distinction matters enormously 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:
Notice the path[:] when saving results. 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 one of the most common bugs in backtracking solutions.
This is a Python-specific trap that catches even experienced developers:
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 most fundamental data structure in Python and the starting point for almost every DSA problem.
This is one of the most common Python bugs in interviews:
The [[0] * cols] * rows bug is one of the most common Python mistakes in interviews. 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. If an interviewer sees this bug, it signals unfamiliarity with 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 biggest advantages 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 one of the most common patterns you will write in matrix problems:
For 8-directional movement (including diagonals), add the four diagonal pairs: (-1,-1), (-1,1), (1,-1), (1,1).
Notice how clean the Python version is compared to Java. Tuple unpacking with for dr, dc in directions and the chained comparison 0 <= nr < rows are both more readable than their Java equivalents.
Strings in Python are immutable. Every time you "modify" a string, Python creates a new object. This has a critical 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 deserves special attention. Since 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.
Unlike Java, Python's == operator compares string content, not references:
This is one less thing to worry about compared to Java.
This is the most important section of this chapter. Python's built-in dictionaries and sets, combined with the collections module, provide the data structures you will reach for in virtually every DSA problem. Choosing the right collection is often the difference between an O(n) and an O(n^2) solution.
dict is the Python equivalent of Java's HashMap. It provides O(1) average-case lookups, inserts, and deletes. Since Python 3.7, dictionaries maintain insertion order.
Key operations:
A critical 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. This is the Python equivalent of Java's NullPointerException when unboxing a null map.get() result.
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 incredibly powerful for DSA:
set provides O(1) average-case membership testing:
The fact that add() does not raise an error when the element already exists means you can use it to detect duplicates by checking membership first:
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 essential for LRU cache implementations:
This concept is critical 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 |
Java has no built-in Pair or Tuple class, forcing you to use int[] arrays or custom classes. Python has tuples, and they are one of the language's biggest advantages for DSA.
1. Tuples are hashable (unlike lists), so they can be dict keys and set elements:
2. Tuples compare lexicographically, which is incredibly 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. It is essential 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. 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.
Java has TreeMap and TreeSet built into the standard library. Python does not have a built-in sorted container, but it provides two alternatives: the bisect module for binary search on sorted lists, and the third-party sortedcontainers library (available on LeetCode).
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 O(log n) sorted data structures that are Python's equivalent of Java's TreeMap, TreeSet, and sorted list:
Comparison with Java TreeMap/TreeSet operations:
| Java TreeMap/TreeSet | Python SortedList/SortedDict | Notes |
|---|---|---|
floorKey(x) | sl[sl.bisect_right(x) - 1] | Largest element <= x |
ceilingKey(x) | sl[sl.bisect_left(x)] | Smallest element >= x |
lowerKey(x) | sl[sl.bisect_left(x) - 1] | Largest element < x |
higherKey(x) | sl[sl.bisect_right(x)] | Smallest element > x |
firstKey() | sl[0] | Smallest element |
lastKey() | sl[-1] | Largest element |
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 for multi-key sorting is very powerful. 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. These are not just stylistic preferences. They can significantly reduce the number of lines you write in an interview, leaving you 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 particularly powerful for sliding window problems:
While list comprehensions are generally preferred, map() can be useful for type conversions:
This is one of Python's biggest advantages for coding interviews. The @cache and @lru_cache decorators turn any recursive function into a memoized solution with zero boilerplate. Many DP problems that require 20+ lines of bottom-up tabulation in Java can be solved with a decorated recursive function in Python.
@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 right tool:
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 simple but essential:
| 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 |
Python does not throw Java's ConcurrentModificationException, but modifying a dictionary or set during iteration causes a RuntimeError. Modifying a list during iteration causes silent bugs.
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 significantly slower than Java and C++. While this rarely causes issues on LeetCode (time limits are adjusted per language), understanding 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."".join().