You have the syntax now — variables, loops, functions, data structures, classes. Here is the part that lasts: how to think about a problem before you touch the keyboard.
In the age of AI, typing code is the cheap part. The durable edge is judgment — deciding what to build, how the pieces fit, and whether a result is correct. That judgment rests on three habits: decompose the problem, choose the right data structure, and keep an intuition for efficiency. Fundamentals plus this big-picture design work are exactly what AI tools cannot do for you.
flowchart LR P["Big problem"] --> D["decompose into steps"] D --> S["pick the right data structure"] S --> F["write one function per step"] F --> V["verify & measure"] style D fill:#3776ab,color:#fff style S fill:#3776ab,color:#fff
Decompose first
Take "analyze a block of text." Do not write one giant function that does everything. Split the job into steps you can name and test on their own:
count_words(text)— how often each word appearstop_word(freq)— given those counts, which word winsreport(...)— turn the answer into something a person can read
Each piece is small, obvious, and reusable elsewhere. If a step turns out to be hard, it becomes its own little sub-problem that you solve on its own. That act of breaking a blob of work into named pieces is decomposition, and it is the single highest-leverage habit in programming.
def count_words(text):
freq = {}
for word in text.split():
freq[word] = freq.get(word, 0) + 1
return freq
def top_word(freq):
best = None
for word, count in freq.items():
if best is None or count > freq[best]:
best = word
return best
def report(text):
freq = count_words(text)
winner = top_word(freq)
return winner + " appeared " + str(freq[winner]) + " times"
print(report("the cat the dog the bird the cat"))
Match the structure to the question
How you store data decides how fast you can answer questions about it, so pick the shape before you write the loop. The right container makes the question almost trivial:
- Count how many times a word appeared? A dict mapping word to count answers immediately.
- Ask whether something has been seen before? A set answers immediately.
- Keep things in arrival order, with duplicates allowed? A list is built for exactly that.
Choosing well up front is far cheaper than rewriting later. A program that walks a ten-thousand-item list on every request feels slow for a reason no amount of clever code will fix.
flowchart LR Q["'fish' in collection?"] --> L["list: cat, dog, bird, fish ... checked one by one"] Q --> S["set: jump straight to the slot for 'fish'"] L --> R["same answer"] S --> R style L fill:#b45309,color:#fff style S fill:#3776ab,color:#fff
Why the structure changes the speed
The same question can be fast or slow depending on how you stored the data. Asking word in word_list makes Python scan every entry in turn until it finds a match — double the entries and you roughly double the work. People call that linear, or O(n).
Asking word in word_set or word in word_dict does something different: Python runs the key through a hash function to compute exactly which slot it lives in, then checks that one slot. Double the data and the work barely grows. That is constant, or O(1).
You do not need to memorize the notation — just feel the gap between a scan that grows and a lookup that barely notices.
words = ["cat", "dog", "bird", "fish", "ant"]
word_list = words # a list keeps every item in order
word_set = set(words) # a set is built for fast membership checks
# Both answer the question correctly - was "fish" ever seen?
print("fish" in word_list) # True, but the list checks each item in turn
print("fish" in word_set) # True, and the set jumps straight to the answer
# A missing word is reported the same way by both
print("lion" in word_list) # False
print("lion" in word_set) # False
Verify, then trust nothing on faith
Decomposed functions are easy to check because each one is small. Once count_words works on its own, you can trust it inside report. Test the pieces: feed top_word a tiny frequency dict where you already know the winner, and confirm it returns the right key.
If it does not, the bug is local — not smeared across a hundred-line function you cannot hold in your head. This is why decomposition pays off twice: it makes code reusable and checkable. A function you cannot test in isolation is a function you will be afraid to change.
You must check whether each of 10,000 IDs is valid, one lookup at a time. Which structure makes each lookup essentially instant (O(1))?
Sets and dicts hash their keys, so membership checks are O(1) on average. Scanning a list is O(n) — it slows as the list grows.
Build a SeenTracker class that remembers words it has already processed, using a set so each check is O(1). .check(word) returns True if the word was seen before this call, and False if it is new — and it remembers new words.
class SeenTracker:
def __init__(self):
# a set to remember seen words
pass
def check(self, word):
# return True if already seen, else remember it and return False
pass
Store a set in init: self.seen = set().
If word in self.seen return True; otherwise add it and return False.
class SeenTracker:
def __init__(self):
self.seen = set()
def check(self, word):
if word in self.seen:
return True
self.seen.add(word)
return False
This top_word(text) is meant to return the word that appears most often. It builds the counts correctly but then returns the first word instead of the most frequent one. Fix the return so it picks the word with the highest count.
def top_word(text):
freq = {}
for word in text.split():
freq[word] = freq.get(word, 0) + 1
# BUG: this returns the first word, not the most frequent one
return text.split()[0]
Loop over freq.items() to compare each word's count.
Track the best word so far; replace it whenever you find a larger count.
def top_word(text):
freq = {}
for word in text.split():
freq[word] = freq.get(word, 0) + 1
best = None
for word, count in freq.items():
if best is None or count > freq[best]:
best = word
return best
You have a list of (student, score) pairs from several rounds, and each student may appear many times. Write best_scores(records) that returns a dict mapping each student to their single highest score. Use a dict so each student is updated in one pass — do not sort.
def best_scores(records):
# build a dict: student -> highest score seen so far
pass
Loop over records, unpacking each pair into student and score.
If the student is new, or this score beats the stored one, update best[student].
def best_scores(records):
best = {}
for student, score in records:
if student not in best or score > best[student]:
best[student] = score
return best
Recap
- Decompose a big problem into small, named functions you can test one at a time.
- Pick the container to fit the question: dicts count, sets track membership, lists keep order.
- Hashing is why a dict or set finds a key without scanning, so get the shape right early.
- Each small function is also a unit you can verify on its own, and that is what makes the whole program trustworthy.
Next you will assemble these habits into programs that read real input and produce real answers, and the choices you make here are what keep those programs fast as they grow.