Speeding Up Slow Python Code With LLMs in November 2026: Profile Before You Ask, Paste the Real Hotspots, and Benchmark Every Change

"Can you make this faster?" is one of the most common things people paste into a chat window, and one of the least useful. Without numbers, a model will do what it does best: suggest changes that look like optimizations. It swaps a loop for a comprehension, adds a cache, maybe recommends rewriting the module in Rust. Some of those changes help. Many touch code that was never the slow part, and a few quietly change behavior. This guide shows a workflow for speeding up Python code with an LLM where the profiler decides what to work on and a benchmark decides whether a change stays.

A mechanical Minerva stopwatch

You can't speed up what you haven't timed. Start every performance session with a measurement. Image: Minerva Stopwatch by Ansgar Koreng, CC BY-SA 4.0, via Wikimedia Commons.

Why "make it faster" goes wrong

Performance problems are rarely where they look like they are. Developers guess wrong about hotspots all the time, and a model reading source code alone is guessing too. The usual failure modes:

  • Optimizing cold code. The model rewrites a parsing function that takes 2% of the runtime while the real cost is a database call inside a loop three files away.
  • Micro-tweaks with no measurable effect. Replacing for loops with map() or caching attribute lookups can matter in a tight inner loop, but most of the time the difference disappears in noise.
  • Behavior changes dressed as speedups. Adding @lru_cache to a function that reads the current time or a mutable global makes it faster and wrong. Switching a list to a set drops duplicates and ordering.
  • No before-and-after numbers. Without a baseline, nobody can tell whether the change helped, hurt, or did nothing.

The fix is simple to state: measure first, give the model the measurements, and measure again after every change.

Step 1: Build a repeatable way to reproduce the slowness

Before profiling, make the slow thing happen on demand. That might be a script that processes one real input file, a test that calls the slow endpoint handler directly, or a small command that runs the batch job on a sample. Two rules:

  • Use realistic input. Code that is fast on 100 rows can be quadratic on 100,000. If production data is sensitive, generate a sample with the same size and shape.
  • Record the baseline. Run it a few times and write down the wall-clock time. Even time python run_job.py sample.csv is enough to start.

This also gives you a correctness check. Save the output of the current version so you can compare results after each change.

Step 2: Profile before you ask anything

Python ships with cProfile, which records how many times each function is called and how much time it takes. Run it on your reproduction script and sort by cumulative time:

python -m cProfile -o job.prof run_job.py sample.csv
python -c "import pstats; pstats.Stats('job.prof').sort_stats('cumulative').print_stats(25)"

The output lists functions with columns such as ncalls, tottime (time spent in the function itself), and cumtime (time including everything it calls). Look for two patterns: a single function with a large tottime, and a cheap function with a huge ncalls count. Both point to where work is actually happening.

A few other tools are worth knowing:

  • py-spy is a sampling profiler that can attach to a running process without code changes, for example py-spy top --pid 12345, or record a flame graph with py-spy record -o profile.svg -- python run_job.py. It is useful for long-running services where you cannot easily rerun the slow path.
  • line_profiler shows time per line inside functions you decorate with @profile, run through kernprof -l -v run_job.py. Use it once cProfile has narrowed things down to one or two functions.
  • timeit from the standard library is the right tool for comparing two small snippets, and it handles repetition for you.

Step 3: Give the model the hotspots, not the whole repo

Now the model has something real to work with. A good prompt includes:

  1. The top 15 to 25 lines of profiler output, pasted as text.
  2. The source of the two or three functions that dominate the profile.
  3. The input size and the baseline time.
  4. Your Python version and the key libraries involved, such as pandas or SQLAlchemy, with versions.
  5. Constraints: what must not change (output order, duplicates, error handling) and what you are willing to add (a new dependency or not).

Then ask a narrow question, for example:

Here is cProfile output for a job that takes 48 seconds on a 200,000-row file,
and the source of the three functions with the highest cumtime.
Python 3.12, pandas 2.x.
1. Using only this profile, explain where the time is going. Quote the rows you rely on.
2. Propose at most three changes, ranked by expected impact.
3. For each change, say exactly what behavior could differ (ordering, duplicates, errors, memory).
Do not suggest changes to functions that are not in this profile.

Asking the model to quote the profiler rows it relies on keeps its explanation tied to evidence. If it claims a function is slow but cannot point to a line in the output, treat that claim as a guess.

A Williams Formula One crew practicing a pit stop

Pit crews shave time by drilling the steps that actually cost seconds. Profiling points you at those steps in your code. Image: Pit Stop Practice at Williams by pedrik, CC BY 2.0, via Wikimedia Commons.

Step 4: Know the fixes that usually matter

Most real speedups in everyday Python come from a short list of patterns. When the model proposes something, it helps to recognize which category it falls into:

  • Wrong data structure. Checking if item in some_list inside a loop scans the list every time. Building a set or dict once turns that into a constant-time lookup on average. Remember that a set drops duplicates and ordering.
  • Repeated work in a loop. Opening a file, compiling a pattern, building a lookup table, or calling an API inside a loop that could do it once outside.
  • Queries in a loop. The classic "N+1" pattern: one database or HTTP call per item. Batching into one query or one bulk request often dwarfs every other optimization.
  • Row-by-row pandas. df.iterrows() and df.apply with a Python function per row are slow compared to vectorized column operations, merge, or map with a dictionary.
  • String building. Accumulating a large string with += in a loop is slower and less predictable than collecting pieces in a list and calling "".join(parts).
  • Caching pure functions. functools.lru_cache or functools.cache can help a lot when the same arguments repeat, but only when the function's result depends on nothing except its arguments.

Be skeptical of bigger moves, such as switching to multiprocessing, adding async, or rewriting in a compiled language, until the simple fixes are exhausted. They add complexity, and they will not fix an algorithm that does far more work than it needs to.

Step 5: Benchmark every change, one at a time

Apply one suggestion, then rerun the same reproduction and compare against the baseline. For small snippets, compare with timeit:

python -m timeit -s "items = list(range(10000)); lookup = set(items)" "9999 in items"
python -m timeit -s "items = list(range(10000)); lookup = set(items)" "9999 in lookup"

For the whole job, run it several times and look at the spread, not a single number. Laptops throttle, caches warm up, and background processes interfere. If a change saves less than the run-to-run variation, it did not meaningfully help, and you should consider reverting it to keep the code simpler. For more rigorous comparisons, the pyperf package is designed to produce stable benchmark results.

Keep a short log as you go:

baseline           48.2s  (3 runs: 47.9, 48.2, 48.6)
set for id lookup  19.4s  output identical
batch DB queries    6.1s  output identical
cache parse_date    5.9s  within noise, reverted

That log is useful in the pull request description, and it stops you from keeping changes that only felt faster.

Step 6: Prove the output did not change

Speed means nothing if the result is wrong. After each change:

  • Run the existing test suite.
  • Diff the new output against the saved baseline output. For files, a plain diff or a checksum works. For data frames, compare with pandas.testing.assert_frame_equal, deciding up front whether row order matters.
  • Test the edge cases the model flagged: empty input, duplicates, missing values, and errors that used to be raised and should still be raised.

If the output differs, ask the model to explain the difference using the actual diff rather than letting it guess. Sometimes the old behavior was a bug, but that is a decision for you to make on purpose, not a side effect of an optimization.

Step 7: Profile again

Once the biggest hotspot is fixed, the profile looks different. Something that was 5% of runtime may now be 40%. Rerun the profiler and repeat the cycle with the new top entries. Stop when the job meets the target you actually need, such as "under 10 seconds" or "fits inside the cron window", rather than chasing every last millisecond.

A quick checklist

  • Reproduce the slowness with realistic input and record a baseline time and output.
  • Profile with cProfile or py-spy before asking for suggestions.
  • Paste the profiler rows, the hot functions, versions, and constraints into the prompt.
  • Make the model quote the profile rows behind each claim and list behavior risks.
  • Apply one change at a time and benchmark it against the baseline over several runs.
  • Diff outputs and run tests after every change; revert anything within noise.
  • Re-profile and stop when you hit the target.

Used this way, an LLM is a good performance partner. It reads profiler output quickly, knows the common Python pitfalls, and can explain trade-offs clearly. The profiler tells it where to look, the benchmark tells you whether it worked, and your saved output tells you whether anything broke along the way.

Comments

Popular posts from this blog

Grok Bot - a step closer to AGI

Structured Outputs and Constrained Decoding for Reliable LLM APIs in Production (September 2026)

Tencent Hy4 Preview: Open 770B MoE Built for Real Work