The only skill AI genuinely can't replace
AI tools are extraordinary at generating code. They're considerably less reliable at thinking about problems. Given a vague prompt, they'll produce plausible-looking code that may be solving the wrong problem, or solving the right problem inefficiently. The person who catches this is the engineer who can think clearly about what the problem actually is before any code is written.
This is the skill that makes the difference between a vibe coder who gets into trouble and an agentic engineer who catches the trouble before it ships.
The four questions before you write anything
Whenever you get a problem โ from a colleague, from your own head, or from an AI agent you're directing โ ask these four questions first:
- What is the input? What data do I receive? What are the edge cases (empty, null, very large, negative, duplicate)?
- What is the output? What exact form should the result take?
- What are the constraints? Does this need to be fast? Does it run in a tight loop? Is memory limited?
- What's the simplest correct solution first? Not the cleverest โ the simplest. Optimize only if needed.
This sounds obvious. It is obvious. And most bugs exist because someone skipped these questions.
Big-O: what you actually need to know
You don't need to derive Big-O proofs. You need intuition โ the ability to look at a piece of code and feel whether it will be fast or slow. Here's the practical version:
The intuition table
| Complexity | What it means | Spot this pattern |
|---|---|---|
O(1) |
Instant โ doesn't depend on input size | Dict lookup, list index access |
O(log n) |
Very fast โ halves the problem each step | Binary search, sorted tree lookup |
O(n) |
Linear โ one pass through the data | Single loop, list traversal |
O(n log n) |
Good for sorting | Python's sorted() |
O(nยฒ) |
Slow on large inputs โ gets bad fast | Nested loops โ this is the one to spot |
O(2โฟ) |
Terrible โ doubles with every new element | Recursive functions without memoization |
A nested loop over the same list is O(nยฒ). It works fine for 100 items. It becomes painfully slow at 10,000 items. AI tools generate nested loops constantly when a hash map lookup would be O(n). Spotting this and suggesting the fix is a real, valued skill.
The hash map replacement
The single most common performance upgrade in code review: replace a "find X in list" with a dict lookup.
# โ O(nยฒ) โ for each user, scan the ENTIRE permissions list
def get_user_permissions(users: List[User], permissions: List[Permission]):
result = []
for user in users:
for perm in permissions: # nested loop = O(nยฒ)
if perm.user_id == user.id:
result.append((user, perm))
return result
# โ O(n) โ build the lookup dict once, then use it
def get_user_permissions(users: List[User], permissions: List[Permission]):
perm_by_user = {p.user_id: p for p in permissions} # O(n) once
return [
(user, perm_by_user[user.id])
for user in users
if user.id in perm_by_user # O(1) per lookup
]
Three patterns worth knowing cold
These three patterns solve a surprising fraction of coding problems. Recognising which one applies is the decomposition skill.
1. Hash map โ "I need to look something up fast"
Any time a problem involves finding duplicates, counting occurrences, grouping items, or checking if something exists โ reach for a dict first.
2. Two pointers โ "I need to compare elements across a sorted list"
Two pointers that move toward each other (or in the same direction at different speeds) replace nested loops for many array problems.
# Find two numbers that sum to target โ two pointer approach
def two_sum_sorted(nums: List[int], target: int) -> Optional[Tuple[int, int]]:
left, right = 0, len(nums) - 1
while left < right:
total = nums[left] + nums[right]
if total == target:
return (nums[left], nums[right])
elif total < target:
left += 1
else:
right -= 1
return None
3. Sliding window โ "I need the best/sum/max over a sub-range"
When you need to examine every contiguous subarray of a fixed size, a sliding window avoids recomputing from scratch each time.
# Maximum sum of any 3 consecutive numbers โ sliding window
def max_sum_window(nums: List[int], k: int) -> int:
window_sum = sum(nums[:k])
max_sum = window_sum
for i in range(k, len(nums)):
window_sum += nums[i] - nums[i - k] # add new, remove old
max_sum = max(max_sum, window_sum)
return max_sum
Decomposition in practice
Here's how you apply the "four questions" to a real problem before writing code. Imagine you're asked: "Given a list of load records, find all flights where the actual weight exceeds the maximum by more than 10%."
- Input: A list of dicts with at least
flight_number,actual_weight,max_weight. Edge cases: empty list, records wheremax_weightis 0 (division by zero!), missing keys. - Output: A list of flight numbers (strings), possibly with the excess percentage.
- Constraints: Could be thousands of records โ O(n) is fine, but no nested loops over the same list.
- Simplest solution: A single list comprehension with a guard for
max_weight > 0.
def find_overloaded_flights(
records: List[Dict[str, float]],
threshold_pct: float = 10.0
) -> List[str]:
"""Return flight numbers where actual weight exceeds max by threshold_pct%."""
return [
r["flight_number"]
for r in records
if r.get("max_weight", 0) > 0
and (r["actual_weight"] - r["max_weight"]) / r["max_weight"] * 100 > threshold_pct
]
Notice: the division-by-zero guard came from step 1 (edge cases). Without the four questions, that bug would have been in the code.
The Codewars habit
Codewars presents small, well-defined programming puzzles. You solve them, then see how other people solved them. That second step โ reading other solutions after you've written yours โ is where the real learning happens.
The daily routine: one kata before you start work. Takes 10โ15 minutes. After solving it, read the top-voted solutions. Ask yourself: why is their version more elegant? What did they know that I didn't?
Start at 8 kyu (easiest) and work your way up. Once you're consistently solving 6 kyu with clean solutions, you're ready for Module A3.