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 t1
and t2
are 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 t1
or t2
occur during this phase. As a result, their difference remains zero.
Exit Phase: Only t2
gets updated to the current timestamp when the context manager exits. This step "locks in" the end time. The difference t2 - t1
then 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 in self.time
for 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.