Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
357: Timing Code with the timeit Module
Every now and then, you'll hit a point in your project where you're staring at two different ways to solve the same problem and you find yourself wondering, "Which one of these is actually faster?" Maybe you're deciding between a list comprehension and a map() call, or you're wondering if a specific library method is more efficient than a custom loop. It's a natural instinct to want to measure the performance, but how you do that measurement matters a lot more than you might think.
The trap of the manual stopwatch
The first thing most developers do—and I'll admit, I did this for years—is the "manual stopwatch" approach. You import the time module, grab the timestamp right before the code starts, grab it right after it ends, and subtract the two. It looks something like this:
import time
start = time.time()
# The code we want to test
result = ""
for i in range(1000):
result += str(i)
end = time.time()
print(f"Elapsed time: {end - start}")
On the surface, this seems logical. But in a real-world OS, your Python script isn't the only thing running. Your browser is open, your antivirus is scanning in the background, and the OS is constantly swapping tasks. If a background process spikes right as you run your test, your "measurement" is actually measuring the OS overhead, not your code. Even worse, time.time() isn't always high-resolution enough for small snippets of code, meaning you might just get 0.0 seconds for things that actually take a few milliseconds.
Why timeit is the professional choice
This is why Python provides the timeit module. Instead of running your code once and hoping for the best, timeit runs the code thousands (or millions) of times and gives you the average. It effectively "smooths out" the noise caused by background system processes. It also temporarily disables garbage collection during the timing run, so you aren't accidentally measuring a random GC cycle that happened to trigger during your test.
Here is how I'd handle that same string concatenation test using timeit:
import timeit
# We define the code we want to test as a string
naive_concat = """
result = ""
for i in range(1000):
result += str(i)
"""
join_method = """
result = "".join(map(str, range(1000)))
"""
# Run each 10,000 times
time_naive = timeit.timeit(stmt=naive_concat, number=10000)
time_join = timeit.timeit(stmt=join_method, number=10000)
print(f"Naive: {time_naive:.4f}s")
print(f"Join: {time_join:.4f}s")
Dealing with the namespace gap
You'll notice in the example above that I put the code inside strings. This is where timeit can get a bit annoying. Because it runs the code in an isolated environment to keep things clean, it doesn't have access to the variables or imports in your current script. If you try to time a function you've already written, you'll get a NameError.
To fix this, you use the setup argument. The setup code is executed once before the timing loop starts and isn't included in the final time measurement. I usually use this to import modules or define the functions I'm testing:
import timeit
def my_complex_logic(n):
return sum(i**2 for i in range(n))
# We pass the function name and the setup string to tell timeit where to find it
t = timeit.timeit(
stmt="my_complex_logic(100)",
setup="from __main__ import my_complex_logic",
number=10000
)
print(f"Execution time: {t:.4f}s")
The from __main__ import ... trick is my go-to way to bring local functions into the timeit scope. It keeps your test code clean and prevents you from having to rewrite your logic as a giant string.
📋 Practical Task
Benchmarking List Construction Methods
You are optimizing a data pipeline and need to determine which method of creating a list of squares is most efficient for a medium-sized dataset. Create a script that uses the timeit module to compare the following three approaches:
- A standard
forloop using.append() - A list comprehension
- The
map()function combined with alambda
Each test should calculate the squares of numbers from 0 to 5,000. Run each test 1,000 times. Your script should print the total time taken for each method and identify which one was the fastest. Ensure you use the setup argument if you define your tests in separate functions.
There are no comments for now.