In the previous lesson we defined what a data structure is and left one question open: if several structures can hold the same data, does it really matter which one you choose? In this lesson you are going to confirm that it does — not with theory, but with a stopwatch in hand: we will see how the same TaskFlow operation — finding a task — can take microseconds or seconds depending on the chosen structure. We will also see that the importance goes beyond performance: it affects scalability, code readability, and even your professional career.

Contents

  1. Choosing a structure is a design decision, not a detail
  2. Performance: the TaskFlow lookup experiment
  3. Scalability: what works with 10 doesn't work with a million
  4. Readability and maintainability: code that explains itself
  5. Professional impact: technical interviews and real work

Choosing a structure is a design decision, not a detail

When a program runs slowly or becomes hard to maintain, the beginner's instinct is to look for the problem in the algorithms or in the language ("Python is slow"). But very often the problem lies earlier: in how the data was organized. A famous remark by Linus Torvalds, the creator of Linux, sums it up: good programmers worry about data structures and their relationships more than about the code itself.

The choice of structure shapes four aspects of your software:

  • Performance: how long each operation takes.
  • Scalability: whether that time remains acceptable as the data grows.
  • Readability: whether the code expresses its intent clearly.
  • Maintainability: whether it can be changed tomorrow without breaking everything.

Let's look at each one using TaskFlow as our test bench.

Performance: the TaskFlow lookup experiment

TaskFlow needs a seemingly trivial operation: given a task's identifier, retrieve it. It is the application's most frequent operation: every time the user opens a task, edits it, or marks it as done, it first has to be found.

First approach: we store the tasks in a list and search by scanning through it.

def create_tasks(n):
    """Generates n sample tasks for TaskFlow."""
    return [
        {"id": i, "title": f"Task {i}", "status": "pending"}
        for i in range(n)
    ]

def find_in_list(tasks, target_id):
    """Scans the list until it hits the task (sequential search)."""
    for task in tasks:
        if task["id"] == target_id:
            return task
    return None

Let's explain the code:

  • create_tasks uses a list comprehension to manufacture n task dictionaries, with ids 0, 1, 2, .... It lets us simulate boards of different sizes.
  • find_in_list examines the tasks one by one, in order, until it finds the one with the target id. If the id is at the end — or doesn't exist — it will have looked at all of them.

Second approach: alongside the list, we maintain an index: a dictionary that maps each id to its task.

def build_index(tasks):
    """Builds a dictionary id -> task."""
    return {task["id"]: task for task in tasks}

def find_in_index(index, target_id):
    """Asks for the task directly by its key."""
    return index.get(target_id)

Here build_index walks over the tasks exactly once and assembles the dictionary; from that moment on, index.get(id) returns the task without scanning anything (how the dictionary pulls this off will be revealed in module 5; for now, treat it as well-documented magic).

Now let's measure. Python ships with the timeit module, designed precisely for timing code snippets by repeating them many times and giving a reliable result:

import timeit

tasks = create_tasks(1_000_000)      # one million tasks
index = build_index(tasks)
worst_id = 999_999                   # the last one: the worst case for the list

t_list = timeit.timeit(
    lambda: find_in_list(tasks, worst_id), number=10
)
t_index = timeit.timeit(
    lambda: find_in_index(index, worst_id), number=10
)

print(f"List:  {t_list:.4f} s for 10 lookups")
print(f"Index: {t_index:.6f} s for 10 lookups")

Details of the experiment:

  • number=10 tells timeit to run the lookup 10 times and add up the times.
  • We search for id 999_999 on purpose: being at the end, it forces the list to scan everything. (The underscores in 1_000_000 are just Python's visual separators; they don't change the number.)
  • The lambda wraps the call so that timeit can run it repeatedly.

Typical results on an ordinary laptop (yours will vary in the figures, not in the conclusion):

Number of tasks List search (worst case) Index lookup Approximate difference
10 0.0000005 s 0.00000005 s ~10×
10,000 0.0004 s 0.00000005 s ~8,000×
1,000,000 0.04 s 0.00000005 s ~800,000×

Read the table slowly, because it contains the entire lesson:

  • With 10 tasks, both solutions are instantaneous. Any structure will do.
  • With a million, the list takes about 800,000 times longer than the index on every lookup. And a real application searches constantly: if TaskFlow serves 100 lookups per second, the list version simply cannot keep up.
  • The index doesn't even flinch as the data grows: it takes practically the same time with 10 as with a million.

You don't yet need formal vocabulary to name this phenomenon (that vocabulary, Big O notation, is the topic of lesson 01-04). The intuition is enough: one structure is forced to look at everything; the other goes straight to it.

Scalability: what works with 10 doesn't work with a million

The previous experiment illustrates the most treacherous mistake in software development: code that works perfectly in testing and collapses in production. It is treacherous because there is no warning: no syntax error, no exception, the tests pass. It's just that with real data, everything grinds to a crawl.

graph LR
    A[Development: 10 test tasks] -->|everything is fine| B[Demo: 500 tasks]
    B -->|everything is fine| C[Production: 1,000,000 tasks]
    C -->|the app crawls| D[Rewrite? More servers?]
    D -->|the cause was| E[A structure poorly chosen on day one]

Thinking about scalability means asking, for every structure you choose: what will happen to this operation when the data grows a thousandfold? Sometimes the answer is "nothing serious, this collection will never grow" (the three statuses of a TaskFlow task will always be three), and a simple structure is the right choice. Other times the answer forces a design change. What matters is asking the question in time: changing a structure on day one costs minutes; changing it with the application in production can cost weeks.

An honest caveat: the indexed structure isn't free either. Building the index takes time and occupies extra memory (we're storing references to every task twice!). In our case it pays off handsomely, because it is built once and queried millions of times. This kind of trade-off — paying some memory or setup time in exchange for fast operations — will come up again and again throughout the course.

Readability and maintainability: code that explains itself

Performance is not the only reason to choose well. A well-suited structure makes the code say what it does. Compare these two ways of managing the statuses a TaskFlow task can have:

# Option A: statuses "by hand" with loose variables
pending = ["Design logo", "Send invoice"]
in_progress = ["Write report"]
done = []

def move_to_in_progress(title):
    if title in pending:
        pending.remove(title)
        in_progress.append(title)
# Option B: a dictionary of status -> tasks
board = {
    "pending": ["Design logo", "Send invoice"],
    "in_progress": ["Write report"],
    "done": [],
}

def move(title, source, target):
    if title in board[source]:
        board[source].remove(title)
        board[target].append(title)

Both work, but notice the differences:

  • In option A, adding a fourth status ("blocked", for example) means creating another variable and writing new move functions for every combination. In B, adding a key to the dictionary is enough: move already works for any pair of statuses.
  • Option B expresses the domain's real relationship: "each status has its list of tasks". Anyone reading the code understands the data model at a glance.
  • Option A scatters the program's state across loose variables, multiplying the places where inconsistencies can arise.

The general rule: when the data structure mirrors the structure of the problem, the code simplifies itself. Many convoluted conditionals and duplicated functions are the symptom of a structure that doesn't fit the domain.

Professional impact: technical interviews and real work

It's worth being blunt about this, because it directly affects your career as a junior developer:

  • Technical interviews. Data structures are the heart of technical hiring processes at most companies, from startups to the big tech firms. It is common to be asked to solve a problem and justify which structure you use and why; practice platforms like LeetCode or HackerRank are literally organized by data structure. It's not about memorizing solutions, but about demonstrating the reasoning you are learning in this course: identify the problem's relationships and operations, and choose accordingly.
  • Code reviews. In professional teams, comments like "this is a linear search inside a loop, use a set" are everyday fare. Understanding them — and being able to make them yourself — marks the difference between executing tasks and participating in design.
  • Performance debugging. A large share of slowness problems in real applications are solved without touching sophisticated algorithms: just by replacing a poorly chosen structure, exactly as in our index experiment.
  • Shared vocabulary. When a colleague says "this is a priority queue" or "let's model it as a graph", they are compressing hours of explanation into one sentence. Data structures are the profession's shared language.

And a note for the current era: even when AI assistants generate code for you, deciding whether that code organizes the data well remains your job. In fact, reviewing someone else's code (human or generated) demands more judgment about structures, not less.

Common Mistakes and Tips

  • "With little data it doesn't matter" turned into a habit. It's true that with 10 elements any structure will do, and you shouldn't over-optimize. The mistake is not writing down the assumption. A comment like # NOTE: linear search, fine while there are only a few dozen tasks is worth gold when the application grows.
  • Optimizing without measuring. The opposite reflex is also a mistake: rewriting structures "because it's surely slow" without having timed it. Get used to timeit right away: hunches about performance are wrong remarkably often.
  • Measuring badly. Be careful when timing: run the operations many times (number=...), don't measure a single run (the operating system adds noise), and don't include the cost of preparing the data in the measurement.
  • Confusing "faster structure" with "better structure". The index in our experiment consumes extra memory and must be kept up to date when tasks are added or removed. Almost every structure choice is a trade-off; your job is to know the terms of the exchange.

Exercises

Exercise 1: reproducing the experiment

Copy the functions create_tasks, find_in_list, build_index, and find_in_index from this lesson and measure the worst-case lookup with timeit for sizes 100, 10,000, and 1,000,000. Build your own results table. At what size does the difference start to become noticeable on your machine?

Exercise 2: spotting the weak point

This code checks which users on a guest list are already registered in TaskFlow:

def registered_guests(guests, registered_users):
    result = []
    for guest in guests:
        if guest in registered_users:   # registered_users is a list
            result.append(guest)
    return result

Knowing that guest in registered_users on a list scans the entire list in the worst case: (a) explain in your own words why this code will scale badly if both collections grow; (b) propose an improvement using set (Python sets answer the question "is this element in here?" almost instantly, like the dictionary in the experiment); (c) verify it with timeit for 10,000 guests and 10,000 registered users.

Exercise 3: arguing like in a code review

A colleague proposes storing TaskFlow's action history (to support undo) in a dictionary {action_number: action} plus a separate counter. In 3-5 sentences and without code, write what you would ask them about the operations the history needs, and what maintainability risks you see in keeping the counter separate from the data. (You don't need to propose the ideal structure: that comes in module 3.)

Solutions

Solution 1:

A complete measurement program:

import timeit

for n in (100, 10_000, 1_000_000):
    tasks = create_tasks(n)
    index = build_index(tasks)
    worst = n - 1
    t_list = timeit.timeit(lambda: find_in_list(tasks, worst), number=10)
    t_dict = timeit.timeit(lambda: find_in_index(index, worst), number=10)
    print(f"n={n:>9}: list {t_list:.6f} s | index {t_dict:.6f} s")

Indicative results: with n=100 both are below a millisecond (imperceptible difference); with n=10_000 the list already takes on the order of milliseconds; with n=1_000_000 the difference is several orders of magnitude. The exact figure depends on your machine; the trend does not.

Solution 2:

(a) For each guest, the entire registered list is potentially scanned: with 10,000 guests and 10,000 registered users, up to 100 million comparisons are made. Doubling both collections multiplies the work by four, not by two: the growth explodes.

(b) It's enough to convert the registered users to a set just once:

def registered_guests_v2(guests, registered_users):
    registered = set(registered_users)   # one-time conversion
    return [g for g in guests if g in registered]

g in registered on a set scans nothing: it answers almost instantly (the why, in module 5).

(c) Measurement:

import timeit
guests = [f"user{i}" for i in range(10_000)]
registered = [f"user{i}" for i in range(5_000, 15_000)]

t1 = timeit.timeit(lambda: registered_guests(guests, registered), number=3)
t2 = timeit.timeit(lambda: registered_guests_v2(guests, registered), number=3)
print(t1, t2)   # typical: ~2-4 s versus ~0.003 s

Solution 3:

A good answer should include questions like: which operations will the history perform (add the latest action, retrieve the latest, maybe the last N)? Do we need to access old actions by number, or only the most recent one? Risks of the separate counter: it is duplicated state, so if the dictionary and the counter fall out of sync (through a deletion, a bug, or an oversight), the history becomes corrupt without warning; furthermore, anyone reading the code has to discover on their own that the two belong together. The reasonable conclusion: the usage pattern "last in, first out" calls for a structure that guarantees it by contract, which is exactly what we will see in module 3.

Conclusion

In this lesson you saw, with real measurements, that the choice of data structure matters: looking up tasks in TaskFlow went from taking tenths of a second to being instantaneous when a list was replaced by an index, and the difference grows brutally with data size. You also saw that the importance is not only about performance: a structure that mirrors the domain makes code more readable and maintainable, and mastering this reasoning is among the most valued skills in technical interviews and in daily work.

Now then: to choose a structure you need to know the catalog. Which families of structures exist and what is each one for? That landscape — linear and nonlinear, static and dynamic, plus the full map of what we'll cover in the course — is the topic of the next lesson.

© Copyright 2026. All rights reserved