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.Counter and collections.defaultdict eliminate 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 mid with mid+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): insert val into 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, and deque signatures without looking them up.
  • Implement two_sum cleanly in under 2 minutes — O(n) hashmap approach only.
  • Build a frequency counter and sliding window solution for a novel problem each.