Why does timeit.timeit take a separate setup argument instead of one block of code?
answer
- Two strings, one clock
- Only one of them is timed
- Fixtures live above the first clock read
- Return value covers every iteration
- Your module's names need globals=
basics
~20 stimeit runs the setup string once, before the clock starts, and only the stmt string inside the timed loop. Imports and test-data construction are therefore excluded, so you measure the operation itself rather than its fixtures.
solid answer
~40 s`timeit.timeit(stmt, setup, timer, number, globals)` builds one small function by string substitution: setup is spliced in above the first clock read, stmt goes in a loop body that runs `number` times, and the clock is read on either side of that loop. So the cost of importing a module or building a 1,000-element list belongs in setup and never reaches the result. Two consequences bite immediately: the return value is the **total** seconds for all `number` iterations, so per-call cost is `result / number`; and stmt cannot see your own module's names unless you pass `globals=globals()` or import them in setup. In `timeit.repeat`, setup runs once per repetition — before each timed loop, never inside one.
code
python · 8 linesimport timeit
total = timeit.timeit(
stmt="sorted(data)",
setup="import random; data = [random.random() for _ in range(1000)]",
number=1000,
)
print(f"{total / 1000 * 1e6:.1f} us per sorted() call")go deeper
Be ready to write a two-line timeit call and say which string is timed. Remember to divide the result by number, and to build test data in setup rather than in the statement you are measuring.
Explain the generated function: setup above the first clock read, stmt in the loop, both compiled once. Know why stmt cannot see your module's names without globals=globals(), and why passing a callable adds per-iteration call overhead.
Show that you check what a benchmark actually measures before trusting it — an already-sorted fixture, a list the statement grows, or a build cost hiding inside stmt. Say how you would restructure the split to answer the question you meant to ask.
Own the convention: if the team benchmarks at all, the fixture-versus-operation split and the per-call normalisation should be encoded in a shared harness, not re-invented in each pull request where numbers are quoted.
## The function timeit builds for you `timeit.timeit(stmt='pass', setup='pass', timer=time.perf_counter, number=1000000, globals=None)` does not `eval` your code once per iteration. It substitutes your two strings into a template that looks essentially like this, compiles it once, and calls it: ```python def inner(_it, _timer): <setup goes here> _t0 = _timer() for _i in _it: <stmt goes here> _t1 = _timer() return _t1 - _t0 ``` That single picture answers most questions about the two arguments. Setup sits above `_t0 = _timer()`, so everything it does — imports, building a list, opening a file, seeding a random generator — happens before the clock starts and cannot appear in the measurement. The statement sits in the loop body, so it is the only thing repeated `number` times between the two clock reads. Compilation of both strings also happens once, outside the timed region, which is why passing source as a string is not the overhead people expect. ## What the number you get back means The return value is the elapsed seconds for the whole loop, not for one execution. With the default `number=1000000`, a result of `0.42` means 420 nanoseconds per execution. Forgetting the division is the single most common misreading of a timeit result, and it is why the command line is friendlier for quick work: `python -m timeit -s 'setup' 'stmt'` prints a per-loop figure directly and chooses `number` for you. The loop exists because the clock is not free. `time.perf_counter`, which `timeit.default_timer` is bound to, has fine resolution but a call still costs tens of nanoseconds. Timing a single execution of a 50 ns expression would measure mostly clock overhead. Running it a million times inside one pair of clock reads amortizes that overhead to nothing. ## Namespaces: where the names in your strings come from The generated function is executed with timeit's own module globals unless you pass `globals`. So this fails: ```python def score(bid): return bid * 1.05 timeit.timeit('score(10)') # NameError: name 'score' is not defined timeit.timeit('score(10)', globals=globals()) # works ``` The older idiom is `setup='from __main__ import score'`, which is what the command line usually needs. You can also skip strings entirely and pass zero-argument callables as stmt and setup; the template then calls your callable inside the loop, which adds one Python call — tens of nanoseconds — to every iteration. That is irrelevant for a 10 microsecond body and ruinous for a 50 nanosecond one. One subtlety worth knowing: names bound by setup are assigned inside the generated function, so they are its locals, and references to them from stmt compile to fast local loads. Your snippet therefore behaves like code inside a function, not like code at module level, where the same names would be global lookups. If you are chasing the difference between the two, that matters. ## setup runs once per repetition, not once overall `timeit.repeat(stmt, setup, repeat=5, number=100000)` calls the compiled function five times and returns five totals. Setup runs at the top of each of those calls. That is usually exactly what you want: if stmt consumes or mutates what setup produced, every repetition starts from the same clean state. ## The mistakes this design invites 1. **Work in the wrong string.** `setup='data = sorted(raw)'` with `stmt='sorted(data)'` times sorting an already-sorted list, which Python's sort handles with a single linear scan. The benchmark is not wrong so much as it is answering a different question. 2. **A fixture the statement destroys.** `setup='data = [0] * 1000'` with `stmt='data.append(1)'` and `number=1000000` grows the list to a million elements during the run; you end up timing periodic reallocation, not `list.append`. 3. **Fixture construction left in stmt.** `stmt='sorted([random.random() for _ in range(1000)])'` measures building the list plus sorting it, and the build usually dominates. 4. **Reading the total as a per-call figure.** The discipline the two arguments enforce is simple: everything that establishes the conditions goes in setup; the one operation whose cost you want goes in stmt; and whatever survives into the reported number is what you actually measured. ## Statements longer than one line Neither string is limited to a single expression. Separate short statements with semicolons, or pass a triple-quoted string containing a real block; timeit re-indents each line as it splices it into the template, so a `for` loop or an `if` written normally compiles correctly. On the command line, several positional arguments are joined with newlines, and `-s` may be given more than once, so `python -m timeit -s 'a = 1' -s 'b = 2' 'a + b'` is valid. The longer the statement grows, though, the less useful the answer: a ten-line stmt tells you that ten lines cost something, not which line did. At that point the honest move is to split the block into separate timings, or to stop micro-benchmarking and measure the whole program instead.
- Does the setup string run once overall, or once per repetition, in timeit.repeat?Once per repetition. `repeat` calls the same compiled function r times, and setup is the first thing that function does, before it reads the clock. So a setup that builds a 10 MB list pays for that build r times, but never inside a measurement. It also means each repetition starts from an identical state, which matters when the statement mutates the fixture.
- How do you time a function defined in your own module without re-importing it in setup?Pass `globals=globals()` to `timeit.timeit` or `timeit.Timer`; the generated function then executes with your namespace instead of timeit's. The alternatives are `setup='from __main__ import score'`, which is the usual command-line form, or passing the function object itself as stmt — at the cost of one extra Python call per iteration.
- What exactly does the float returned by timeit.timeit represent?Total elapsed seconds for all `number` executions of stmt, measured with `timeit.default_timer` (which is `time.perf_counter`). Divide by `number` for per-execution cost. The command line does that division for you and prints a per-loop figure; the library function does not.
saying these in an interview costs you the question
- Reads the returned float as the cost of one execution
- Puts the data construction in stmt and times the fixture
- Assumes stmt can see the calling module's names by default
- Thinks setup runs inside the timed loop
- Times a statement that destroys the fixture it needs