Python Linear Search
A linear search (also called a sequential search) is the simplest way to find a value inside a collection: you look at every element, one after another, starting from the beginning, until you either find a match or run out of elements to check. It doesn’t require the data to be sorted, which makes it the most flexible search technique and often the first tool you reach for when working with small or unordered lists. Understanding linear search deeply also builds the intuition you need for faster algorithms like binary search, since linear search is the baseline every other search strategy is compared against.
Overview / How It Works
Linear search walks through a sequence element by element, comparing each one to the target value using ==. If an element matches, the search stops and reports where the match was found (usually its index). If the loop finishes without a match, the search reports that the value isn’t present. There is no cleverness involved — no assumptions about ordering, no skipping ahead — which is exactly why it works on any iterable: a plain list, a tuple, a generator, or even a linked list if you were implementing one from scratch.
Because it inspects up to every element, linear search runs in O(n) time in the worst case and on average, where n is the number of elements. The best case is O(1), when the target happens to be the very first element. Space complexity is O(1) as well, since it only needs a counter/index and no extra data structures. Compare this to binary search, which runs in O(log n) time but requires the data to already be sorted — linear search trades speed for the freedom to work on unsorted data with zero preprocessing.
It’s worth knowing that Python’s built-in tools already do linear search for you in many situations. The in operator (x in some_list) and the list.index() method both scan a list from the start, element by element, at the C level. They’re faster in practice than a hand-written Python for loop because the comparison loop runs in optimized C rather than interpreted bytecode, but the underlying algorithm — and its O(n) complexity — is identical to what you’ll write yourself in this lesson. Writing your own linear_search() function still matters because real searches are rarely just “does this exact value exist” — you often need the index, the first match under a custom condition, every match, or a search over objects rather than plain values, none of which the built-ins hand you directly.
Syntax
The general shape of a linear search function looks like this:
def linear_search(sequence, target):
for index, value in enumerate(sequence):
if value == target:
return index
return -1
| Part | Meaning |
|---|---|
sequence |
The list (or other iterable) to search through |
target |
The value you’re looking for |
enumerate(sequence) |
Produces (index, value) pairs so you can track position while iterating |
value == target |
The comparison that decides whether the current element is a match |
return index |
Exits immediately once a match is found (this is called short-circuiting) |
return -1 |
A sentinel value returned only if the loop finishes without finding a match |
Examples
Example 1: Basic Linear Search
def linear_search(items, target):
for index, value in enumerate(items):
if value == target:
return index
return -1
numbers = [4, 2, 9, 7, 5, 1]
result = linear_search(numbers, 7)
print(f"Index of 7: {result}")
result_missing = linear_search(numbers, 100)
print(f"Index of 100: {result_missing}")
Output:
Index of 7: 3
Index of 100: -1
The function scans numbers from left to right. It checks 4, then 2, then 9, then 7 — at that point value == target is true, so it returns the index 3 immediately without looking at the remaining elements. For 100, which never appears, the loop runs to completion and the function falls through to return -1.
Example 2: Finding All Matching Indices
def find_all_indices(items, target):
return [index for index, value in enumerate(items) if value == target]
scores = [85, 92, 85, 70, 85, 60]
matches = find_all_indices(scores, 85)
print(f"Indices where 85 appears: {matches}")
if matches:
print(f"Found {len(matches)} matches, first at index {matches[0]}")
else:
print("No matches found")
Output:
Indices where 85 appears: [0, 2, 4]
Found 3 matches, first at index 0
A single-match linear search stops at the first hit, but sometimes you need every occurrence. This version doesn’t short-circuit — it’s still a linear scan (still O(n)), but instead of returning early it collects every index where the value matches using a list comprehension built on the same enumerate pattern.
Example 3: Linear Search Over Objects
students = [
{"id": 101, "name": "Ava"},
{"id": 102, "name": "Liam"},
{"id": 103, "name": "Maya"},
]
def find_student(students, student_id):
for student in students:
if student["id"] == student_id:
return student
return None
target_id = 102
found = find_student(students, target_id)
if found is not None:
print(f"Student {target_id}: {found['name']}")
else:
print(f"No student with id {target_id}")
missing = find_student(students, 999)
print(f"Lookup for missing id: {missing}")
Output:
Student 102: Liam
Lookup for missing id: None
This is where hand-written linear search really earns its keep: the built-in in operator can’t search by a field of a dictionary, only by exact equality of a whole value. Here the search compares student["id"] against the target on each iteration and returns the whole matching dictionary, or None if nothing matches. Notice the sentinel changed from -1 to None because the “found value” is now a dictionary, not an index — -1 would be a confusing, and misleading, choice here.
How It Works Step by Step (Under the Hood)
Tracing Example 1 with numbers = [4, 2, 9, 7, 5, 1] and target = 7:
index=0, value=4→4 == 7isFalse, continueindex=1, value=2→False, continueindex=2, value=9→False, continueindex=3, value=7→True,return 3immediately
The for loop is abandoned the instant return executes — Python doesn’t keep checking indices 4 and 5. This early exit is what makes the best case O(1): if the target were at index 0, only a single comparison would run. If the target isn’t present at all (or is the very last element), every element gets compared once, giving the O(n) worst case. An empty list is a degenerate case worth noticing too: enumerate([]) yields nothing, the loop body never executes, and the function falls straight through to return -1 without error.
It also helps to know what CPython does when you use in or list.index() instead of writing your own loop: both walk the underlying array pointer by pointer in C, doing the equivalent of PyObject_RichCompare on each element until a match or the end of the array. Same algorithm, same O(n) behavior, just implemented below the Python interpreter for speed. That’s why x in my_list is usually preferred when you only need a yes/no answer.
Common Mistakes
Mistake 1: Testing the Result with Truthiness Instead of Comparing to the Sentinel
def linear_search(items, target):
for index, value in enumerate(items):
if value == target:
return index
return -1
numbers = [10, 20, 30]
result = linear_search(numbers, 10)
if result:
print("Found")
else:
print("Not found")
Output:
Not found
This is wrong even though 10 is clearly in the list at index 0. The bug is if result: — in Python, 0 is falsy, so when a match is found at index 0, the truthiness check treats it the same as “not found.” Always compare explicitly against the sentinel value instead of relying on truthiness:
def linear_search(items, target):
for index, value in enumerate(items):
if value == target:
return index
return -1
numbers = [10, 20, 30]
result = linear_search(numbers, 10)
if result != -1:
print("Found")
else:
print("Not found")
Output:
Found
Mistake 2: Forgetting That list.index() Raises an Exception
Unlike a hand-written search that returns -1 for “not found,” the built-in list.index() method raises a ValueError when the target isn’t present:
numbers = [4, 2, 9, 7, 5, 1]
index = numbers.index(100)
print(index)
Running this raises ValueError: 100 is not in list and crashes the program if it isn’t handled. Wrap the call in a try/except block so a missing value is handled gracefully instead of crashing:
numbers = [4, 2, 9, 7, 5, 1]
try:
index = numbers.index(100)
print(f"Found at index {index}")
except ValueError:
print("100 is not in the list")
Output:
100 is not in the list
Best Practices
- Use
enumerate(sequence)instead ofrange(len(sequence))when you need both the index and the value — it’s more readable and more Pythonic. - If you only need a yes/no membership check, prefer the built-in
inoperator (target in items) over a hand-written loop — it does the same linear scan but faster, in C. - Pick a sentinel that can never be confused with a real result (
-1for indices,Nonefor objects) and always compare against it explicitly rather than relying on truthiness. - Reach for linear search on small, unsorted, or one-off searches; if you’ll search the same collection many times, convert it to a
setordictonce for O(1) average-case lookups instead of repeating an O(n) scan every time. - If the data is sorted and stays sorted, use
bisect(binary search) instead — O(log n) beats O(n) once the list is large. - Keep the comparison logic (
value == target, or a custom condition likestudent["id"] == student_id) as the only thing inside the loop’sif— don’t mix in unrelated side effects that make the search harder to reason about. - Add type hints (
def linear_search(items: list, target) -> int:) on utility search functions so callers know what to expect back.
Practice Exercises
- Exercise 1: Write a function
last_index(items, target)that returns the index of the last occurrence oftargetinitems(instead of the first), or-1if it isn’t present. Hint: you can scan from the end, or scan forward and keep overwriting a stored result. - Exercise 2: Write a function
count_occurrences(items, target)that counts how many timestargetappears initemsusing your own loop (don’t uselist.count()). Foritems = [1, 3, 3, 5, 3, 7]andtarget = 3, it should return3. - Exercise 3: Given
products = [{"name": "Pen", "price": 2}, {"name": "Book", "price": 15}, {"name": "Eraser", "price": 1}], writefind_cheaper_than(products, max_price)that returns the first product dictionary whose"price"is less thanmax_price, orNoneif none qualify.
Summary
- Linear search checks each element in order until it finds a match or reaches the end — it needs no sorting and works on any iterable.
- Time complexity is O(1) best case, O(n) worst and average case; space complexity is O(1).
enumerate()is the idiomatic way to get both index and value while scanning.- Always use an unambiguous sentinel (
-1orNone) for “not found,” and compare against it explicitly — never rely on truthiness, since a valid result like index0is falsy. list.index()andinperform the same linear scan internally but raiseValueError(forindex()) instead of returning a sentinel, so handle that withtry/exceptwhen needed.- For repeated lookups on the same data, a
setordictbeats linear search; for one-off or unsorted searches, linear search is simple, correct, and hard to beat for clarity.
