Python Coding Patterns
Writing clean, idiomatic, efficient Python under time pressure. CodeSignal DS assessments test fluency with NumPy, Pandas, and core Python patterns — not just knowledge of algorithms.
Key Takeaways
- Prefer vectorized NumPy/Pandas operations over loops — orders of magnitude faster
- List comprehensions are 2–5x faster than for-loops for small iterables
collections.Counterandcollections.defaultdicteliminate boilerplate for frequency maps and grouped data- Know
zip(),enumerate(),sorted(key=...)by heart — they appear constantly in assessments - Time complexity: dict/set lookup is O(1); unsorted list search is O(n); sort is O(n log n)
- String
.join()is O(n); repeated+=on strings is O(n²) — always use join for building strings
1. NumPy Essentials
Array Creation and Vectorized Operations
NumPy arrays replace loops with compiled C operations. The performance difference is routinely 10–100x for large arrays.
import numpy as np
arr = np.array([1, 2, 3, 4, 5])
zeros = np.zeros((3, 4))
arange = np.arange(0, 10, 0.5) # start, stop, step
linspace = np.linspace(0, 1, 100) # start, stop, num_points
# Vectorized arithmetic — no loops needed
arr * 2 # [2, 4, 6, 8, 10]
arr ** 2 # [1, 4, 9, 16, 25]
np.sqrt(arr)
np.log(arr) # natural log
# Broadcasting: add a row vector to each row of a matrix
matrix = np.arange(12).reshape(3, 4) # shape (3, 4)
row = np.array([1, 2, 3, 4]) # shape (4,)
matrix + row # broadcast along axis 0
Boolean Indexing and Masking
arr = np.array([10, -5, 3, -1, 8, 0])
positive = arr[arr > 0] # [10, 3, 8]
arr[arr < 0] = 0 # clip negatives in-place
# np.where: vectorized if/else
np.where(arr > 0, arr, 0) # positive values, else 0
np.where(arr > 5, 'high', 'low')
Aggregations
arr = np.array([[1, 2, 3], [4, 5, 6]])
arr.sum() # 21 (all elements)
arr.sum(axis=0) # [5, 7, 9] — column sums
arr.sum(axis=1) # [6, 15] — row sums
arr.mean(axis=1) # [2.0, 5.0]
np.percentile(arr, [25, 50, 75])
arr.argmax(axis=1) # index of max per row
2. Efficient Python Patterns
Comprehensions
# List comprehension — prefer over for-loop
squares = [x**2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
# Dict comprehension
word_len = {word: len(word) for word in ['apple', 'banana', 'fig']}
# Nested — flatten a 2D list
flat = [x for row in matrix for x in row]
collections Module
from collections import Counter, defaultdict, deque
# Counter — frequency map in one line
text = "the quick brown fox jumps over the lazy dog"
counts = Counter(text.split()) # {'the': 2, 'quick': 1, ...}
counts.most_common(3)
# defaultdict — no KeyError on missing keys
graph = defaultdict(list)
graph['A'].append('B') # works even if 'A' not yet in dict
# deque — O(1) popleft (list.pop(0) is O(n))
queue = deque([1, 2, 3])
queue.appendleft(0) # O(1)
queue.popleft() # O(1)
Built-in Functions
names = ['Alice', 'Bob', 'Carol']
scores = [88, 92, 79]
# zip — iterate multiple sequences together
for name, score in zip(names, scores):
print(f"{name}: {score}")
# enumerate — index + value, no counter variable
for i, name in enumerate(names, start=1):
print(f"{i}. {name}")
# sorted with key
people = [('Alice', 30), ('Bob', 25), ('Carol', 35)]
by_age = sorted(people, key=lambda x: x[1])
oldest = max(people, key=lambda x: x[1]) # ('Carol', 35)
# String join: always O(n) — prefer over repeated +=
sentence = ' '.join(['data', 'science', 'at', 'capital', 'one'])
3. Time Complexity Cheat Sheet
| Operation | Data Structure | Complexity |
|---|---|---|
| Lookup / insert / delete | dict / set | O(1) avg |
| Lookup by index | list | O(1) |
| Search (unsorted) | list | O(n) |
| Search (sorted) | list + bisect | O(log n) |
| Insert at head | list | O(n) — use deque |
| Insert at head | deque | O(1) |
| Sort | list | O(n log n) |
| Vectorized op | NumPy array | O(n), very low constant |
4. Common CodeSignal Patterns
Sliding Window — O(n)
def max_sum_subarray(arr, k):
"""Max sum of any contiguous subarray of length k."""
window = sum(arr[:k])
best = window
for i in range(k, len(arr)):
window += arr[i] - arr[i - k]
best = max(best, window)
return best
HashMap for O(1) Lookup
def two_sum(nums, target):
seen = {} # value -> index
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return []
Frequency Counter (avoid O(n²) nested loops)
from collections import Counter
def find_duplicates(arr):
counts = Counter(arr)
return [k for k, v in counts.items() if v > 1]
5. Graph Algorithms
Graph problems appear frequently in DS coding assessments. Master BFS for shortest paths in unweighted graphs, DFS for connectivity/cycle detection, and topological sort for dependency ordering.
Graph Representation
from collections import defaultdict, deque
# Adjacency list (preferred for sparse graphs)
graph = defaultdict(list)
edges = [(0,1),(0,2),(1,3),(2,3),(3,4)]
for u, v in edges:
graph[u].append(v)
graph[v].append(u) # omit for directed graphs
Breadth-First Search (BFS)
Explores level by level. Finds shortest path in unweighted graphs. Time: $O(V + E)$.
def bfs_shortest_path(graph, start, end):
if start == end:
return [start]
visited = {start}
queue = deque([(start, [start])]) # (node, path)
while queue:
node, path = queue.popleft()
for neighbor in graph[node]:
if neighbor not in visited:
new_path = path + [neighbor]
if neighbor == end:
return new_path
visited.add(neighbor)
queue.append((neighbor, new_path))
return [] # no path exists
# BFS for level-order / minimum steps
def bfs_levels(graph, start):
dist = {start: 0}
queue = deque([start])
while queue:
node = queue.popleft()
for neighbor in graph[node]:
if neighbor not in dist:
dist[neighbor] = dist[node] + 1
queue.append(neighbor)
return dist
Depth-First Search (DFS)
Explores one branch fully before backtracking. Use for connected components, cycle detection, path existence. Time: $O(V + E)$.
def dfs_recursive(graph, node, visited=None):
if visited is None:
visited = set()
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs_recursive(graph, neighbor, visited)
return visited
def dfs_iterative(graph, start):
visited = set()
stack = [start]
while stack:
node = stack.pop()
if node not in visited:
visited.add(node)
stack.extend(graph[node])
return visited
# Count connected components
def count_components(n, edges):
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
visited = set()
components = 0
for node in range(n):
if node not in visited:
dfs_iterative_mark(graph, node, visited)
components += 1
return components
Topological Sort (Kahn's Algorithm)
For directed acyclic graphs (DAGs). Returns nodes in dependency order. Detects cycles if not all nodes are processed. Use for task scheduling, build systems, course prerequisites.
def topological_sort(n, prerequisites):
graph = defaultdict(list)
in_degree = [0] * n
for course, prereq in prerequisites:
graph[prereq].append(course)
in_degree[course] += 1
queue = deque([i for i in range(n) if in_degree[i] == 0])
order = []
while queue:
node = queue.popleft()
order.append(node)
for neighbor in graph[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return order if len(order) == n else [] # empty = cycle detected
6. Binary Search
Binary search runs in $O(\log n)$ and applies beyond sorted arrays — any time a decision space is monotonic (if a value works, all values on one side also work), binary search on the answer.
Standard Template
import bisect
# Built-in: bisect_left returns first index where val could be inserted (< val or == val)
arr = [1, 3, 5, 7, 9]
idx = bisect.bisect_left(arr, 5) # → 2 (first position with value >= 5)
idx = bisect.bisect_right(arr, 5) # → 3 (first position with value > 5)
# Manual template (handles edge cases safely)
def binary_search(arr, target):
lo, hi = 0, len(arr) - 1
while lo <= hi:
mid = lo + (hi - lo) // 2 # avoids integer overflow
if arr[mid] == target:
return mid
elif arr[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1 # not found
# Find leftmost index where condition becomes True (binary search on answer)
def first_true(lo, hi, condition):
"""Returns smallest x in [lo, hi] where condition(x) is True."""
while lo < hi:
mid = (lo + hi) // 2
if condition(mid):
hi = mid
else:
lo = mid + 1
return lo
Binary Search on Answer Pattern
# Classic: can we allocate work across k workers such that max load <= capacity?
def can_allocate(tasks, k, max_load):
workers = 1
current = 0
for t in tasks:
if t > max_load:
return False
if current + t > max_load:
workers += 1
current = t
else:
current += t
return workers <= k
def min_max_load(tasks, k):
lo, hi = max(tasks), sum(tasks)
return first_true(lo, hi, lambda cap: can_allocate(tasks, k, cap))
Common Patterns
- Search in rotated sorted array: find pivot with binary search, then search the right half
- Find peak element: compare
midwithmid+1; move toward the larger side - Sqrt(x): binary search on integers in $[0, x]$; return last value where $\text{mid}^2 \leq x$
bisect.insort(arr, val): insertvalinto sorted list in $O(\log n + n)$; useful for maintaining a sorted running window
7. Dynamic Programming and Heaps
Dynamic Programming
DP solves problems by breaking them into overlapping subproblems and storing results to avoid recomputation. Key insight: identify a recurrence relation, then implement with memoization (top-down) or tabulation (bottom-up).
from functools import lru_cache
# --- Memoization (top-down) ---
@lru_cache(maxsize=None)
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
# --- Tabulation (bottom-up) ---
def coin_change(coins, amount):
"""Minimum coins to make amount. Returns -1 if impossible."""
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for amt in range(1, amount + 1):
for coin in coins:
if coin <= amt:
dp[amt] = min(dp[amt], dp[amt - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
# --- Longest Common Subsequence ---
def lcs(s1, s2):
m, n = len(s1), len(s2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
# --- 0/1 Knapsack ---
def knapsack(weights, values, capacity):
n = len(weights)
dp = [[0]*(capacity+1) for _ in range(n+1)]
for i in range(1, n+1):
for w in range(capacity+1):
dp[i][w] = dp[i-1][w] # don't take item i
if weights[i-1] <= w:
dp[i][w] = max(dp[i][w], dp[i-1][w-weights[i-1]] + values[i-1])
return dp[n][capacity]
heapq — Priority Queue
Python's heapq is a min-heap. Negate values for max-heap. $O(\log n)$ push and pop;
$O(n)$ heapify.
import heapq
# Min-heap
heap = [3, 1, 4, 1, 5]
heapq.heapify(heap) # O(n) in-place
heapq.heappush(heap, 2) # push
smallest = heapq.heappop(heap) # pop smallest: O(log n)
# Max-heap: negate values
max_heap = [-x for x in [3, 1, 4, 1, 5]]
heapq.heapify(max_heap)
largest = -heapq.heappop(max_heap)
# K largest / K smallest
nums = [3, 1, 4, 1, 5, 9, 2, 6]
k_largest = heapq.nlargest(3, nums) # [9, 6, 5]
k_smallest = heapq.nsmallest(3, nums) # [1, 1, 2]
# Pattern: running median with two heaps
class MedianFinder:
def __init__(self):
self.lo = [] # max-heap (negate): lower half
self.hi = [] # min-heap: upper half
def add(self, num):
heapq.heappush(self.lo, -num)
heapq.heappush(self.hi, -heapq.heappop(self.lo))
if len(self.hi) > len(self.lo):
heapq.heappush(self.lo, -heapq.heappop(self.hi))
def median(self):
if len(self.lo) > len(self.hi):
return -self.lo[0]
return (-self.lo[0] + self.hi[0]) / 2
# Pattern: K closest points to origin
def k_closest(points, k):
return heapq.nsmallest(k, points, key=lambda p: p[0]**2 + p[1]**2)
DP Complexity Cheat Sheet
| Problem | State | Time | Space |
|---|---|---|---|
| Fibonacci | dp[i] | $O(n)$ | $O(1)$ optimized |
| Coin Change | dp[amount] | $O(n \cdot \text{amount})$ | $O(\text{amount})$ |
| LCS | dp[i][j] | $O(mn)$ | $O(mn)$ or $O(\min(m,n))$ |
| 0/1 Knapsack | dp[i][w] | $O(n \cdot W)$ | $O(n \cdot W)$ |
| Longest Increasing Subseq. | dp[i] | $O(n^2)$ or $O(n \log n)$ | $O(n)$ |
Practice Questions
Q1: What is the time complexity of looking up a value in a Python dict vs. a list? Why does this matter in DS assessments?
Dict lookup is O(1) average (hash table). List lookup (search) is O(n). This matters when you need to check membership repeatedly — always convert a list to a set or dict before repeated lookups.
Example: two_sum is O(n²) with nested loops but O(n) with a hashmap. In
a timed assessment, the naive approach times out on large inputs; the hashmap approach
passes easily.
# O(n^2) — nested loop, fails on large inputs
def two_sum_slow(nums, target):
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
# O(n) — hashmap
def two_sum_fast(nums, target):
seen = {}
for i, num in enumerate(nums):
if target - num in seen:
return [seen[target - num], i]
seen[num] = i
Q2: Rewrite this code more efficiently:
result = []; [result.append(x**2) for x in range(1000000) if x % 2 == 0]
Two improvements: (1) use a list comprehension directly instead of appending inside a comprehension side-effect, and (2) for large arrays, use fully vectorized NumPy:
# Better: direct list comprehension
result = [x**2 for x in range(1_000_000) if x % 2 == 0]
# Best for large arrays: fully vectorized NumPy
import numpy as np
arr = np.arange(1_000_000)
result = arr[arr % 2 == 0] ** 2 # boolean mask + vectorized square
The NumPy version is orders of magnitude faster for large inputs because all operations run in compiled C with no Python loop overhead.
Q3: You have a list of (customer_id, transaction_amount) tuples. In one line, build a dict mapping each customer_id to their total spend.
The cleanest production-grade approach uses Pandas groupby:
import pandas as pd
from collections import defaultdict
transactions = [('C1', 50.0), ('C2', 30.0), ('C1', 20.0), ('C3', 100.0)]
# Pandas: most readable and performant for large data
totals = pd.DataFrame(transactions, columns=['cid', 'amt']).groupby('cid')['amt'].sum().to_dict()
# Pure Python with defaultdict (no Pandas dependency)
totals = defaultdict(float)
for cid, amt in transactions:
totals[cid] += amt
totals = dict(totals)
Avoid the pattern
{cid: sum(amt for c,amt in transactions if c==cid) for cid in ...} — it is
O(n²) because it scans the full list for every unique customer.
Q4: What does np.where(arr > 0, arr, -arr) compute? What is the
equivalent pure-Python expression?
It computes the absolute value of each element: positive values are kept, negative values
are negated. This is equivalent to np.abs(arr).
Equivalent pure Python:
arr = [3, -1, 0, -7, 5]
# List comprehension equivalent
result = [x if x > 0 else -x for x in arr]
# Or using built-in map
result = list(map(abs, arr))
# NumPy — vectorized, fastest for large arrays
import numpy as np
arr_np = np.array(arr)
result = np.where(arr_np > 0, arr_np, -arr_np) # same as np.abs(arr_np)
The NumPy version is vastly faster for large arrays because it avoids the Python interpreter loop entirely.
Given a list of prerequisites [course, prereq], determine if you can finish all n courses (detect if a cycle exists).
This is a cycle detection problem in a directed graph — use topological sort (Kahn's algorithm). Build an adjacency list and compute in-degrees. Initialize a queue with all nodes with in-degree 0 (no prerequisites). Process each node: decrement its neighbors' in-degrees and enqueue neighbors whose in-degree reaches 0. If all n nodes are processed, no cycle exists and you can finish. If fewer than n nodes are processed, there is a cycle. Time: O(V + E). Alternative: DFS with three states — unvisited, visiting (in current path), visited — a cycle exists if you reach a 'visiting' node.
How would you find the K-th largest element in a stream of integers efficiently?
Maintain a min-heap of size K. For each new element: if the heap has fewer than K
elements, push it. Otherwise, if the element is larger than the heap's minimum, pop the
minimum and push the new element. The K-th largest is always the heap's root (the
minimum of the K largest seen so far). Time per element: O(log K). Space: O(K). This is
the core of LeetCode 703 "Kth Largest Element in a Stream." For a one-off query on an
array, use heapq.nlargest(k, arr)[-1] or the O(n) QuickSelect algorithm.
Action Items
- Given a list of transactions, compute a 7-day rolling average using only NumPy (no Pandas).
- Rewrite a nested for-loop solution into a vectorized NumPy solution
and benchmark the difference with
%timeit. - Memorize
Counter,defaultdict, anddequesignatures without looking them up. - Implement
two_sumcleanly in under 2 minutes — O(n) hashmap approach only. - Build a frequency counter and sliding window solution for a novel problem each.