How it works
The explanation, with examples you can run.
Measure first
cProfile records every function call and how long it took:
import cProfileimport pstats def parse(lines): return [line.split(",") for line in lines] def find_dupes(rows): seen, dupes = [], [] for row in rows: if row[0] in seen: # a list: every check scans it dupes.append(row) seen.append(row[0]) return dupes lines = [f"{i % 5000},x,y" for i in range(20_000)]with cProfile.Profile() as prof: find_dupes(parse(lines))pstats.Stats(prof).sort_stats("cumulative").print_stats(4)On Python 3.12 it reported 0.188 s in total, of which 0.183 s was find_dupes itself (tottime), while parse took 0.003 s. The culprit is row[0] in seen: membership in a list compares against every element, so the function is quadratic. Making seen a set (and append an add) dropped the whole run to 0.007 s. No micro-optimisation would have come close.
For production processes you can't restart under a profiler, a sampling profiler that attaches from outside (py-spy is a popular one) shows where time goes with almost no overhead. Python 3.12 also added -X perf so Linux perf can see Python function names.
Why loops in Python are slow, and how to avoid them
Every iteration of a Python loop runs several instructions, each dispatched by the interpreter and each manipulating full objects. Built-ins do the same loop inside one C function:
import timeit nums = list(range(1_000_000)) def loop_sum(): total = 0 for n in nums: total += n return total t_loop = min(timeit.repeat(loop_sum, number=10, repeat=5))t_builtin = min(timeit.repeat(lambda: sum(nums), number=10, repeat=5))print(f"loop: {t_loop:.3f}s sum(): {t_builtin:.3f}s ratio: {t_loop / t_builtin:.1f}x")On an Apple-silicon laptop with Python 3.12 this printed a ratio of 6.8x. The same reasoning favours "".join(parts) over repeated +=, comprehensions over append loops, sorted(key=...) over hand-written comparisons, and collections.Counter or dict lookups over nested searches. Local variables are also cheaper than globals and attribute lookups, which is why hot loops sometimes bind append = out.append first.
When you need C
If the hot loop is numeric, express it as whole-array operations with NumPy, which stores numbers in compact C arrays and runs the loop in compiled code. For custom logic, compile it: Cython, mypyc (which compiles type-annotated Python), Rust through PyO3, or a hand-written C extension using the C API. Compiled extensions can release the GIL around long computations, so they also scale across threads.
Two interpreter developments are worth knowing about but not counting on. Since 3.11 the specialising interpreter has sped up ordinary code with no changes on your part. Python 3.13 added an experimental JIT compiler (PEP 744), but it is only included when CPython is built with --enable-experimental-jit, so the regular python.org 3.13 installers don't use it. Upgrading Python is often the cheapest speed-up available.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.