AI News Hub Logo

AI News Hub

The Living Giant Python Syntax and Traps LeetCode Document

DEV Community
Tomer Ben David

Yes, this document is long and that is entirely by design. It serves as the ultimate all in one compilation of every technical Python tip, idiom, syntax pattern, and common trap you will encounter when solving LeetCode questions. It is designed to be your continuously updatable single source of truth. The goal is to help you internalize syntax so completely that you can focus all your mental energy entirely on algorithmic logic and problem solving during high pressure interviews. The document is organized strictly from basic Python syntax fundamentals to advanced algorithmic structures. Variables & Syntax Basics Math & Numbers Strings & Characters Iteration & Loops Lists & Arrays Dictionaries & Sets Queues & Stacks Heaps Linked Lists Trees & Graphs Advanced Patterns (Intervals, Sliding Window) Recursion & Caching # Unpack list/tuple a, b, c = [1, 2, 3] # Unpack with rest first, *rest = [1, 2, 3, 4, 5] # first=1, rest=[2,3,4,5] *start, last = [1, 2, 3, 4, 5] # start=[1,2,3,4], last=5 # Unpack in loop queries = [[0, 1], [1, 3]] for x, y in queries: print(x, y) arr = [0, 1, 2, 3, 4, 5] arr[1:4] # [1, 2, 3] arr[:3] # [0, 1, 2] (from start) arr[3:] # [3, 4, 5] (to end) arr[::-1] # [5, 4, 3, 2, 1, 0] (reverse) arr[::2] # [0, 2, 4] (every 2nd element) arr[-1] # Last element arr[-2] # Second to last element One of the most common Python traps is using the * operator to initialize a list of lists (or any container). [[]] * n This does not create n independent empty lists. In Python, list * n is essentially repeated concatenation (+). # Create 3 "references" to the SAME inner list g = [[]] * 3 # is equivalent to: g = [list_a] + [list_a] + [list_a] # Add a neighbor to node 0 g[0].append(1) print(g) # Expected: [[1], [], []] # Actual: [[1], [1], [1]] 0: # Use 'if stack' instead ... In Python, it is a common convention to use an underscore (_) as a variable name when you need to loop for a specific number of times but don't actually need the index within the loop body. This technique is often used in linked list patterns, such as finding the kth node from the end. def find_node(head, k): slow = head fast = head # Use _ because we don't need the index value for _ in range(k): fast = fast.next while fast: slow = slow.next fast = fast.next return slow _? Readability: It explicitly signals to other developers (and your future self) that the loop index is intentionally ignored. Linting: Many linters will warn about unused variables. Using _ is the standard way to bypass these warnings for loop indices. Clarity: It keeps the focus on the loop's purpose (repetitive action) rather than the iteration state. # Traditional swap with temp variable temp = a a = b b = temp # Python's elegant way using tuple unpacking a, b = b, a # Swap array elements arr[i], arr[j] = arr[j], arr[i] all([True, True, False]) # False (all must be True) any([True, False, False]) # True (any must be True) sum([1, 2, 3]) # 6 len([1, 2, 3]) # 3 range(5) # 0, 1, 2, 3, 4 (stops before 5) range(1, 5) # 1, 2, 3, 4 (start inclusive, end exclusive) range(0, 10, 2) # 0, 2, 4, 6, 8 (step by 2) bin(5) # '0b101' (binary string) '110'.count('1') # 2 (count occurrences in string) i >> 1: Shift right by 1 (same as i // 2). i & 1: Get the last bit (same as i % 2). When calculating the middle of two numbers, the order of operations matters. [!CAUTION] Wrong: mid = low + high // 2 high // 2 first, then adds it to low. Example: low=10, high=20. Calculation: 10 + (20 // 2) = 20. You never found the middle! [!TIP] Right: mid = (low + high) // 2 mid = low + (high - low) // 2 to avoid integer overflow. sum() Python allows you to sum up items in a single line, but the placement of the loop matters. [!CAUTION] Wrong: total = sum(math.ceil(x / y)) for x in items sum() on a single number, then tries to start a loop afterward. [!TIP] Right: total = sum(math.ceil(x / y) for x in items) ... for ... in ... expression must be inside the sum() parentheses. This is called a "generator expression." It's fast and memory-efficient. //) vs. True Division (/) [!CAUTION] The Trap: If you use range(), list[index], or binary search pointers, you MUST use an integer. Using / will cause a TypeError because it always returns a float (e.g., 4 / 2 = 2.0). [!TIP] The Fix: Use floor division // to ensure you get an integer (e.g., 5 // 2 = 2). list.append() and list.sort() return None In Python, methods that modify a list in-place return None. You cannot chain them. [!CAUTION] Wrong: result.append(val).count('1') result.append(val) returns None. You are trying to call None.count('1'). [!TIP] Right: val_count = val.count('1') result.append(val_count) [start:stop] The second argument is the stop index, NOT the length. [!CAUTION] Wrong: bin(i)[2:n] (thinking you want n characters). [!TIP] Right: bin(i)[2:] (to slice from index 2 to the end). Arithmetic operators (+, -, *, /) have higher precedence than bitwise operators (&, |, ^). [!CAUTION] Wrong: dp[i >> 1] + i & 1 (dp[i >> 1] + i) & 1 [!TIP] Right: dp[i >> 1] + (i & 1) 10 % 3 # 1 (remainder) 10 // 3 # 3 (division without remainder) # //= is floor division assignment operator curr = 10 curr //= 3 # Same as: curr = curr // 3 # Result: curr = 3 (integer division, no remainder) # Common in sliding window problems curr //= nums[left] # Divide curr by nums[left] using integer division 2 ** 3 # 8 pow(2, 3) # 8 In many languages (Java, C++, Go), integers have a fixed size (usually 32 or 64 bits). If you exceed $2^{63}-1$, the number "wraps around" or overflows, leading to negative results and broken logic. In Python, integers have arbitrary precision. They will grow to consume as much memory as your computer has. When solving problems like Maximum Width of Binary Tree, you need to index nodes as $2i, 2i+1$. In a tree with depth 1000, the index would be $2^{1000}$. Java/C++: You must "normalize" the level (subtract the leftmost index) to prevent overflow. Python: You can ignore overflow entirely. Just keep doubling the numbers. Python will handle the $300$-digit number without breaking a sweat. Python's int is actually a struct that points to a list of "digits" (usually in base $2^{30}$). As the number gets larger, Python dynamically allocates more "digits" to store it. Performance: While arbitrary precision is convenient, math on $10,000$-digit numbers is slower than math on $64$-bit hardware-level integers. Memory: If you create enough massive integers, you can eventually hit a MemoryError. The "Normalize" Habit: In a real interview, even if you use Python, you should mention the normalization technique. It shows "Senior Signal"—that you understand how lower-level memory works and are aware that your code might not be portable to other languages without it. min(1, 2, 3) # 1 max([1, 2, 3]) # 3 min(arr, key=len) # Min by length import math # Initialize a number to infinity max_pattern_count = math.inf # Positive infinity min_value = -math.inf # Negative infinity # Common use case: Initialize min to infinity when finding minimum min_val = math.inf for num in nums: min_val = min(min_val, num) def digit_sum(num): digit_sum = 0 while num: digit_sum += num % 10 # Get last digit num //= 10 # CRITICAL: Use //= not /= (integer division) return digit_sum # Example: digit_sum(123) -> 1 + 2 + 3 = 6 # num % 10 gets the last digit (remainder when dividing by 10) # num //= 10 removes the last digit (integer division by 10) # CRITICAL: Must use //= (integer division), not /= (floating point division) # WRONG: num /= 10 # This creates float, breaks the loop condition # CORRECT: num //= 10 # Integer division, removes last digit correctly Python does not allow direct arithmetic on characters (like char + 1 or char - 'a'). Instead, you must use the "bridge" functions to convert between characters and their ASCII/Unicode integer values. ord(char): Character $\rightarrow$ Integer (ASCII code) chr(int): Integer $\rightarrow$ Character # 1. Increment/Decrement next_char = chr(ord('a') + 1) # 'b' prev_char = chr(ord('z') - 1) # 'y' # 2. Get Alphabetical Index (0-25) # Mnemonic: "Python makes you say 'ord' out loud" index = ord('c') - ord('a') # 2 # 3. Handle Wrap-around (z -> a) char = 'z' next_wrapped = chr((ord(char) - ord('a') + 1) % 26 + ord('a')) # 'a' Task C++ / Java Python Index c - 'a' ord(c) - ord('a') Shift 'a' + 2 chr(ord('a') + 2) Use ord/chr: When you need to iterate through the alphabet or treat letters like a numeric range. Use a lookup string: When the alphabet is custom or small (e.g., "ACGT"). alphabet = "ACGT" next_gene = alphabet[(alphabet.index('A') + 1) % 4] # 'C' In grid problems, never use x and y for variables. This is a common trap that leads to "Cartesian Thinking" and swap bugs. In geometry: x = horizontal (left/right) = Columns y = vertical (up/down) = Rows In coding: grid[x][y] often leads to people checking 0 row change for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)): nr, nc = r + dr, c + dc # Logic remains perfectly consistent: # nr belongs with rows, nc belongs with cols if 0 Height -> compare with len(grid) c (column) -> Width -> compare with len(grid[0]) By using r and c, you physically cannot mix them up. s = "Hello World" s.split() # ['Hello', 'World'] s.split('l') # ['He', '', 'o Wor', 'd'] ''.join(['a', 'b']) # 'ab' s.strip() # Remove whitespace s.replace('l', 'L') # 'HeLLo WorLd' s.startswith('He') # True s.endswith('ld') # True s.count('l') # 2 (Handy for counting characters/bits) # Convert string to array of characters list("hello") # ['h', 'e', 'l', 'l', 'o'] name = "Alice" age = 30 # f-strings (Python 3.6+) f"My name is {name} and I'm {age}" f"Value: {value:.2f}" # Format float to 2 decimals # WRONG: Using $ instead of {} (other languages use $) f"x=$x, y=$y" # Syntax error! # CORRECT: Use curly braces f"x={x}, y={y}" When you need to check if a character is the same letter as another but with the opposite case (common in "string reduction" or "Great String" problems): # The "Double Case" Trick if stack and c == stack[-1].swapcase(): stack.pop() else: stack.append(c) Instead of the verbose: if stack and c.lower() == stack[-1].lower() and c != stack[-1]: The swapcase() method handles the "same letter, different case" logic in a single call. When slicing strings in recursive functions (e.g., s[1:] or s[2:]), you create a new string copy at every call. This requires O(n) memory and time on every single recursion stack frame. # Slicing creates a full copy of the trailing string def dfs_slow(s: str): ... res = dfs(s[1:]) For high performance (like tight LeetCode algorithms with strings of 10,000+ length), pass an expanding index i instead. Looking up an index is O(1). # Passing an index guarantees O(1) step performance def dfs_fast(i: int): # Instead of s[1:], we just access i + 1 if i List[int]: stack = [] # Stores ONLY indices answer = [0] * len(temperatures) for i in range(len(temperatures)): # While the current temp is HIGHER than the temp at the top of the stack # It means we've found the "next warmer day" for the item at the top. while stack and temperatures[stack[-1]] heap[0]: heapq.heapreplace(heap, nums[i]) # O(log K) return heap[0] Mixing Logic: Never use heappop() on a heap created with heapify_max() (pre-3.14). Standard heappop uses Min-Heap logic and will corrupt your Max-Heap structure. heapify In-Place: Remember n = heapify(nums) sets n to None. Index Access: Only heap[0] is guaranteed. heap[-1] is not the maximum/minimum. Tags: #python #heap #priority-queue #data-structures #complexity #top-k When inserting a new node into a single linked list, the order of operations is critical. It can be counter-intuitive because you must update two .next pointers in a specific order to avoid losing the rest of the list. class ListNode: def __init__(self, val): self.val = val self.next = None # Let prev_node be the node at position i - 1 def add_node(prev_node, node_to_add): # 1. First, point the new node to the rest of the list node_to_add.next = prev_node.next # 2. Then, point the previous node to the new node prev_node.next = node_to_add Think of it as "securing the rest of the list first". Step 1 (node_to_add.next = prev_node.next): You first connect your new node to the "tail" of the list (the part that comes after the insertion point). This ensures you have a pointer to the rest of the list. Step 2 (prev_node.next = node_to_add): Once the rest of the list is safely "held" by the new node, you can safely update the prev_node to point to the new node. The Pitfall: If you reversed these steps, you would point prev_node to the new node first. But then you would lose the reference to the original prev_node.next, making it impossible to connect your new node to the rest of the list (the rest of the list becomes "orphaned"). Sentinel nodes (dummy nodes) simplify linked list operations by eliminating edge cases like empty lists or deleting the last node. Head Sentinel: head.next points to the first "real" node. Tail Sentinel: tail.prev points to the last "real" node. Operations are always $O(1)$ when adding/removing from both ends. class ListNode: def __init__(self, val): self.val, self.next, self.prev = val, None, None # Initialization head, tail = ListNode(None), ListNode(None) head.next, tail.prev = tail, head def add_to_start(node): node.prev, node.next = head, head.next head.next.prev = node head.next = node def remove_from_start(): if head.next == tail: return # Empty list to_remove = head.next to_remove.next.prev = head head.next = to_remove.next Without sentinels, you'd need if node.next is None checks everywhere. With sentinels, every "real" node is guaranteed to have a neighbor, so node.next.prev or node.prev.next never fails. If you prefer counting nodes/steps manually instead of using Fast and Slow pointers, you can avoid messy if statements for odd/even lengths by using a specific integer division pattern. If you count the number of jumps (edges) rather than nodes, you have to handle the parity manually unless you use this formula: class Solution: def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]: count = 0 curr = head # Count the "steps" (number of next pointers) while curr.next: count += 1 curr = curr.next # The 'if count % 2 == 0' can be replaced with (count + 1) // 2 mid = (count + 1) // 2 curr = head for _ in range(mid): curr = curr.next return curr (count + 1) // 2? Nodes (length) Jumps (count) Resulting mid Target Index 5 (Odd) 4 (4 + 1) // 2 = 2 2 (Node 3) 6 (Even) 5 (5 + 1) // 2 = 3 3 (Node 4) This pattern ensures you always hit the "second middle" node for even-length lists as required by most LeetCode-style problems, without needing an explicit if check. When reversing a sub-segment of a linked list (e.g., Reverse Linked List II), there are five critical pitfalls to watch out for. Pitfall: Returning the original head pointer. Why: If left = 1, the first node moves, and head is no longer the start of the list. Fix: Always use a dummy node (dummy = ListNode(0, head)) and return dummy.next. Pitfall: Connecting lag.next to the wrong node or creating a cycle. Why: After the loop, prev is the new head of the sub-segment, and cur is the start of the remaining list. Fix: lag.next = prev (Connect the node before the reversal to the new head). left_node.next = cur (Connect the original start of the sub-segment to the rest of the list). Pitfall: Assuming cur is the last node of the reversed segment. Why: The for loop logic or while cur logic usually pushes cur one step past the segment being processed. Fix: prev is on the last processed node (the new head). cur is on the next unprocessed node (the tail's successor). lag Initialization Pitfall: Initializing lag at head. Why: If left = 1, your while index max_sum: max_sum = cur_sum return max_sum No "Off-by-One" Anxiety: By using range(k, len(nums)), you eliminate the need to check r + 1. The loop naturally stops when the data ends. Simplified Pointers: You only manage one index (i). The "left" side of your window is always just i - k. Readability: It’s immediately clear that you are processing the array from the $k$-th element to the end. That while (r + 1) int: if not s: return 1 # ... logic ... By adding @cache directly above the recursive function, it will save the results of past function calls and reuse them if the same arguments are seen again. This drops the complexity from O(2^n) to O(n). When calculating the minimum depth of a tree, a common mistake is to treat a missing child (None) as a path of depth 0. # INCORRECT LOGIC def minDepth(root): if not root: return 0 return min(minDepth(root.left), minDepth(root.right)) + 1 In a tree where a node has only one child (e.g., 1 -> 2), the logic above will: See root.right is None -> return 0. Take min(left_depth, 0) + 1. Conclude the min depth is 1 (as if the root itself was a leaf). A leaf is a node with no left AND no right children. A missing child is an empty set of paths, not a path of length 0. Both children exist: Take min(left, right) + 1. Only one child exists: You must follow that path. Return 1 + depth_of_existing_child. No children (Leaf): Return 1. No root: Return 0. "I must filter out sentinel values (0 for None) before using min(). In maxDepth, this doesn't matter because 0 never wins against a positive depth, but in minDepth, the sentinel 'cheats' the comparison." @cache Decorator (Instant Memoization) In Python, you can convert a slow recursive function into a fast Dynamic Programming (DP) solution by adding a single line. This is the ultimate "cheat code" for top-down DP. Instead of manually creating a memo dictionary and checking if state in memo, just use @cache. from functools import cache @cache def solve(i, j): # Standard recursive logic... return result Phrase: "Just remember what you did so you don't do it again." The WHY: Recursion without memoization is exponential ($O(2^n)$) because it re-solves the same subproblems. @cache automatically stores function arguments as keys and results as values in a hidden dictionary, turning it into $O(N \cdot M)$. The Code: from functools import cache class Solution: def climbStairs(self, n: int) -> int: @cache def dp(i): if i <= 2: return i return dp(i - 1) + dp(i - 2) return dp(n) Hashable Arguments: All arguments to the function (e.g., i, j, state_tuple) must be hashable (integers, strings, tuples). You cannot pass a list or set directly; convert them to a tuple or frozenset first. Recursion Limit: Large constraints might hit Python's default recursion limit (usually 1000). Use sys.setrecursionlimit() if needed. Python Version: @cache was added in Python 3.9. For older versions, use @lru_cache(None). Whenever you are writing Top-Down DP or DFS with memoization. It keeps the code clean and lets you focus on the transition logic rather than state management. Originally posted at: https://looppass.mindmeld360.com/blog/the-living-giant-python-syntax-and-traps-leetcode-document/