Other top-rated answers here can be incorrect
As noted by @Mercury, the other top answer by @Vlad Bezden, while slick, is technically incorrect since the value yielded by t() is also potentially affected by code executed outside of the with statement. For example, if you run time.sleep(5) after the with statement but before the print statement, then calling t() in the print statement will give you ~6 sec, not ~1 sec.
In some cases, this can be avoided by inserting the print command inside the context manager as below:
from time import perf_counter
from contextlib import contextmanager
@contextmanager
def catchtime() -> float:
start = perf_counter()
yield lambda: perf_counter() - start
print(f'Time: {perf_counter() - start:.3f} seconds')
However, even with this modification, notice how running sleep(5) later on causes the incorrect time to be printed:
from time import sleep
with catchtime() as t:
sleep(1)
# >>> "Time: 1.000 seconds"
sleep(5)
print(f'Time: {t():.3f} seconds')
# >>> "Time: 6.000 seconds"
Solution #1: Context Manager Approach (with fix)
This solution captures the time difference using two reference points, t1 and t2. By ensuring that t2 only updates when the context manager exits, the elapsed time within the context remains consistent even if there are delays or operations after the with block.
Here’s how it works:
-
Entry Phase: Both
t1andt2are initialized with the current timestamp when the context manager is entered. This ensures that their difference is initially zero. -
Within the Context: No changes to
t1ort2occur during this phase. As a result, their difference remains zero. -
Exit Phase: Only
t2gets updated to the current timestamp when the context manager exits. This step “locks in” the end time. The differencet2 - t1then represents the elapsed time exclusively within the context.
from time import perf_counter
from time import sleep
from contextlib import contextmanager
@contextmanager
def catchtime() -> float:
t1 = t2 = perf_counter()
yield lambda: t2 - t1
t2 = perf_counter()
with catchtime() as t:
sleep(1)
# Now external delays will no longer have an effect:
sleep(5)
print(f'Time: {t():.3f} seconds')
# Output: "Time: 1.000 seconds"
Using this method, operations or delays outside the with block will not distort the time measurement. Unlike other top-rated answers on this page, this method introduces a level of indirection where you explicitly capture the end timestamp upon exit from the context manager. This step effectively “freezes” the end time, preventing it from updating outside the context.
Solution #2: Class-Based Approach (flexible)
This approach builds upon @BrenBarn’s idea but adds a few usability improvements:
-
Automated Timing Printout: Once the code block within the context completes, the elapsed time is automatically printed. To disable this, you can remove the
print(self.readout)line. -
Stored Formatted Output: The elapsed time is stored as a formatted string in
self.readout, which can be retrieved and printed at any later time. -
Raw Elapsed Time: The elapsed time in seconds (as a
float) is stored inself.timefor potential further use or calculations.
from time import perf_counter
class catchtime:
def __enter__(self):
self.start = perf_counter()
return self
def __exit__(self, type, value, traceback):
self.time = perf_counter() - self.start
self.readout = f'Time: {self.time:.3f} seconds'
print(self.readout)
As in solution #1, even if there are operations after the context manager (like sleep(5)), it does not influence the captured elapsed time.
from time import sleep
with catchtime() as timer:
sleep(1)
# Output: "Time: 1.000 seconds"
sleep(5)
print(timer.time)
# Output: 1.000283900000009
sleep(5)
print(timer.readout)
# Output: "Time: 1.000 seconds"
This approach provides flexibility in accessing and utilizing the elapsed time, both as raw data and a formatted string.