Profile Code with timeit
Learn how to profile Python code performance using the timeit module in this hands-on tutorial. Step-by-step walkthrough, troubleshooting, and next steps for progressive mastery.
Focus: profile code performance with timeit
Have you ever wondered why one Python snippet runs slower than another? Without measuring, you're guessing — and guesses waste optimization time. The timeit module provides a precise, repeatable way to profile code performance with timeit, letting you base speed improvements on hard data instead of hunches.
The problem this lesson solves
Optimizing code without profiling is like fixing a car without opening the hood. You might accidentally slow down a fast section while ignoring the real bottleneck. Manual timing with time.time() or datetime is unreliable because it includes OS overhead, garbage collection pauses, and other background noise. timeit solves this by disabling the garbage collector, running your code many times, and returning a statistically stable average. It answers one critical question: Which of these implementations is actually faster?
Core concept / mental model
Think of timeit as a stopwatch for code snippets that automatically runs a race multiple times and reports the best lap. The module works in two modes:
- Command-line interface – great for quick one-offs.
- Programmatic interface – ideal for embedding in test suites or notebooks.
How timeit works under the hood
- It parses your code string (or callable).
- Sets up an isolated execution environment using
timeit.Timer. - Disables Python's garbage collector to reduce measurement noise.
- Runs the statement repeatedly (
numberparameter) for a givenrepeatcount. - Returns the minimum time among the repeats — this best reflects the fastest possible run, free from outliers.
Pro tip: The
numberargument controls how many times the snippet executes per repeat. Therepeatargument controls how many times the entire timing is performed. The final result is the minimum of all repeats, not the average.
How it works step by step
Step 1: Import timeit
import timeit
Step 2: Write the code as a string or callable
You can pass a raw string, a multi-line string, or a function object.
Step 3: Call timeit.timeit() or timeit.repeat()
timeit.timeit(stmt, setup, number)– runs the statementnumbertimes and returns total seconds.timeit.repeat(stmt, setup, repeat, number)– runstimeit()multiple times and returns a list of results.
Step 4: Interpret the result
The return value is a float representing total seconds. To get per-iteration time, divide by number.
Hands-on walkthrough
Example 1: Compare list creation methods
import timeit
# Using list comprehension
stmt_listcomp = "[i**2 for i in range(100)]"
time_listcomp = timeit.timeit(stmt_listcomp, number=10000)
print(f"List comprehension: {time_listcomp:.5f} sec")
# Using for-loop with append
stmt_forloop = """
result = []
for i in range(100):
result.append(i**2)
"""
time_forloop = timeit.timeit(stmt_forloop, number=10000)
print(f"For loop: {time_forloop:.5f} sec")
Expected output:
List comprehension: 0.02961 sec
For loop: 0.04523 sec
List comprehensions are consistently faster because they avoid repeated attribute lookups and function calls.
Example 2: Use setup for imports
import timeit
# setup runs once before each repeat
setup_code = "import random"
stmt = "random.randint(0, 100)"
time = timeit.timeit(stmt, setup=setup_code, number=100000)
print(f"random.randint time: {time:.5f} sec")
Expected output:
random.randint time: 0.08917 sec
Example 3: Time a function directly
import timeit
def fibonacci(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
# Pass the function object (no quotes!)
time = timeit.timeit(lambda: fibonacci(30), number=1000)
print(f"fibonacci(30) 1000 times: {time:.5f} sec")
Expected output:
fibonacci(30) 1000 times: 0.00234 sec
Pro tip: When passing a callable, use a
lambdawithout arguments. Do NOT include parentheses that call the function —timeitwill call it repeatedly for you.
Example 4: Use repeat() for multiple measurements
import timeit
results = timeit.repeat(
stmt="[i**2 for i in range(100)]",
number=1000,
repeat=5
)
print(f"Results: {results}")
print(f"Min time: {min(results):.5f} sec")
print(f"Max time: {max(results):.5f} sec")
Expected output (variation expected):
Results: [0.00301, 0.00289, 0.00278, 0.00295, 0.00310]
Min time: 0.00278 sec
Max time: 0.00310 sec
The minimum is the most reliable indicator because it's least affected by system load.
Compare options / when to choose what
| Feature | timeit.timeit() |
timeit.repeat() |
Manual time.time() |
|---|---|---|---|
| Best for | Single, quick comparison | Detailed analysis with variance | Quick & dirty timing |
| Returns | Float (total seconds) | List of floats | Float (wall clock) |
| Garbage collector | Disabled automatically | Disabled automatically | Not handled |
| Repeatability | One run only (but number iterations) |
Multiple runs, reports min | Varies heavily every run |
| Overhead isolation | Excellent | Excellent | Poor |
| When to use | Comparing 2–3 snippets | Stability check, benchmarking | Logging, coarse timestamps |
Choice guide:
- Use
timeit.timeit()for simple A/B comparisons between two snippets. - Use
timeit.repeat()when you need confidence in stability (e.g., for performance regression tests). - Avoid manual timing unless you are logging runtime in production where precision isn't critical.
Troubleshooting & edge cases
Mistake 1: Forgetting the setup argument
If your statement imports modules, they must be included in setup. Otherwise, timeit runs them every iteration, distorting results.
# WRONG — import runs 10000 times
wrong = timeit.timeit("import math; math.sqrt(9)", number=10000)
# RIGHT — import runs once
right = timeit.timeit("math.sqrt(9)", setup="import math", number=10000)
Mistake 2: Timing very fast operations without enough number
Operations that finish in microseconds need many iterations to get a stable measurement.
# Too few iterations — result near zero
fast = timeit.timeit("x = 1 + 1", number=10)
print(fast) # might print 0.0
# Enough iterations
fast = timeit.timeit("x = 1 + 1", number=1000000)
print(fast) # prints something like 0.018
Mistake 3: Including I/O or network calls
timeit is meant for CPU-bound code. Disk reads, database queries, or API calls will be dominated by external latency, making results meaningless.
Mistake 4: Using timeit inside function scopes incorrectly
If you define a function inside the statement string, it will be redefined on every iteration. Define it in setup instead.
setup = """
def square(x):
return x*x
"""
stmt = "square(5)"
timeit.timeit(stmt, setup=setup, number=100000)
What you learned & what's next
You now understand how to profile code performance with timeit — from the mental model of a precise stopwatch to hands-on comparisons of list creation, function timing, and multi-run stability. You can confidently choose between timeit.timeit() and timeit.repeat(), and you know how to avoid common pitfalls like missing setup or including I/O.
This skill is the foundation for optimizing real-world Python code. In the next lesson, you'll apply profiling to memory usage and larger-scale bottlenecks using tools like memory_profiler and cProfile. You'll integrate timeit into a systematic performance checklist that covers speed, memory, and algorithmic complexity — turning you into a data-driven optimizer.
Keep measuring before optimizing. Your code (and your users) will thank you.
Practice recap
Open a Python REPL or script and write three small functions that calculate factorial: one using recursion, one using a loop, and one using math.factorial. Profile each with timeit.timeit() using number=10000. Note which is fastest. Then increase number to 100000 and see if the ordering changes.
Common mistakes
- Forgetting the
setupargument when statements require imports, causing the import to run every iteration and inflate the time. - Using too few iterations for fast operations (e.g.,
number=10for1+1) — results become0.0or unstable. - Including I/O-bound operations (disk reads, network calls) inside
timeit— external latency makes timing meaningless. - Defining functions inside the statement string instead of in
setup, causing redefinition every iteration.
Variations
- Use
timeit.repeat()instead oftimeit.timeit()when you need multiple runs and the minimum time for stability analysis. - Pass a callable (function or lambda) directly to
timeitinstead of a string — avoids quoting issues and is cleaner for testing. - For large-scale profiling, combine
timeitwithcProfileto get both fine-grained snippet timing and overall call statistics.
Real-world use cases
- CI/CD pipeline: Automatically benchmark critical functions to catch performance regressions before deploying.
- Algorithm selection: Compare different sorting or search algorithms on realistic data sizes during development.
- Code review: Provide objective timing data to justify choosing a list comprehension over a for-loop in a pull request.
Key takeaways
timeitprovides reliable, repeatable timing by disabling GC and running code many times per repeat.- Use
timeit.timeit()for quick A/B comparisons andtimeit.repeat()for detailed stability checks. - Always place imports and helper definitions in the
setupargument, not in the statement. - The returned value is total seconds for all iterations — divide by
numberto get per-call time. - Profile before optimizing;
timeithelps you base decisions on data, not guesses.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.