Table of Contents
A timer is a powerful tool for monitoring the performance of your Python code. By using thetime.perf_counter()
function, you can measure execution time with exceptional precision, making it ideal for benchmarking. Using a timer involves recording timestamps before and after a specific code block and calculating the time difference to determine how long your code took to run.
In this tutorial, you’ll explore three different approaches to implementing timers: classes, decorators, and context managers. Each method offers unique advantages, and you’ll learn when and how to use them to achieve optimal results. Plus, you’ll have a fully functional Python timer that can be applied to any program to measure execution time efficiently.
By the end of this tutorial, you’ll understand that:
time.perf_counter()
is the best choice for accurate timing in Python due to its high resolution.Along the way, you’ll gain deeper insights into howclasses,decorators, andcontext managers work in Python. As you explore real-world examples, you’ll discover how these concepts can not only help you measure code performance but also enhance your overall Python programming skills.
Decorators Q&A Transcript:Click here to get access to a 25-page chat log from our Python decorators Q&A session in the Real Python Community Slack where we discussed common decorator questions.
First, you’ll take a look at some example code that you’ll use throughout the tutorial. Later, you’ll add aPython timer to this code to monitor its performance. You’ll also learn some of the simplest ways to measure the running time of this example.
If you check out the built-intime
module in Python, then you’ll notice several functions that can measure time:
Python 3.7 introduced several new functions, likethread_time()
, as well asnanosecond versions of all the functions above, named with an_ns
suffix. For example,perf_counter_ns()
is the nanosecond version ofperf_counter()
. You’ll learn more about these functions later. For now, note what the documentation has to say aboutperf_counter()
:
Return the value (in fractional seconds) of a performance counter, i.e. a clock with the highest available resolution to measure a short duration. (Source)
First, you’ll useperf_counter()
to create a Python timer.Later, you’ll compare this with other Python timer functions and learn whyperf_counter()
is usually the best choice.
To better compare the different ways that you can add a Python timer to your code, you’ll apply different Python timer functions to the same code example throughout this tutorial. If you already have code that you’d like to measure, then feel free to follow the examples with that instead.
The example that you’ll use in this tutorial is a short function that uses therealpython-reader
package to download the latest tutorials available here on Real Python. To learn more about the Real Python Reader and how it works, check outHow to Publish an Open-Source Python Package to PyPI. You can installrealpython-reader
on your system withpip
:
$python-mpipinstallrealpython-reader
Then, you canimport the package asreader
.
You’ll store the example in a file namedlatest_tutorial.py
. The code consists of one function that downloads and prints the latest tutorial from Real Python:
latest_tutorial.py
1fromreaderimportfeed 2 3defmain(): 4"""Download and print the latest tutorial from Real Python""" 5tutorial=feed.get_article(0) 6print(tutorial) 7 8if__name__=="__main__": 9main()
realpython-reader
handles most of the hard work:
feed
fromrealpython-reader
. This module contains functionality for downloading tutorials from theReal Python feed.0
is an offset, where0
means the most recent tutorial,1
is the previous tutorial, and so on.main()
when you run the script.When you run this example, your output will typically look something like this:
$pythonlatest_tutorial.py#PythonTimerFunctions:ThreeWaystoMonitorYourCodeA timer is a powerful tool for monitoring the performance of your Pythoncode. By using the `time.perf_counter()` function, you can measure executiontime with exceptional precision, making it ideal for benchmarking. Using atimer involves recording timestamps before and after a specific code block andcalculating the time difference to determine how long your code took to run.[ ... ]## Read the full article at https://realpython.com/python-timer/ »* * *
The code may take a little while to run depending on your network, so you might want to use a Python timer to monitor the performance of the script.
Now you’ll add a bare-bones Python timer to the example withtime.perf_counter()
. Again, this is aperformance counter that’s well-suited for timing parts of your code.
perf_counter()
measures the time in seconds from some unspecified moment in time, which means that the return value of a single call to the function isn’t useful. However, when you look at the difference between two calls toperf_counter()
, you can figure out how many seconds passed between the two calls:
>>>importtime>>>time.perf_counter()32311.48899951>>>time.perf_counter()# A few seconds later32315.261320793
In this example, you made two calls toperf_counter()
almost 4 seconds apart. You can confirm this by calculating the difference between the two outputs: 32315.26 - 32311.49 = 3.77.
You can now add a Python timer to the example code:
latest_tutorial.py
1importtime 2fromreaderimportfeed 3 4defmain(): 5"""Print the latest tutorial from Real Python""" 6tic=time.perf_counter() 7tutorial=feed.get_article(0) 8toc=time.perf_counter() 9print(f"Downloaded the tutorial in{toc-tic:0.4f} seconds")1011print(tutorial)1213if__name__=="__main__":14main()
Note that you callperf_counter()
both before and after downloading the tutorial. You then print the time it took to download the tutorial by calculating the difference between the two calls.
Note: In line 9, thef
before the string indicates that this is anf-string, which is a convenient way to format a text string.:0.4f
is a format specifier that says the number,toc - tic
, should be printed as a decimal number with four decimals.
For more information about f-strings, check outPython’s F-String for String Interpolation and Formatting.
Now, when you run the example, you’ll see the elapsed time before the tutorial:
$pythonlatest_tutorial.pyDownloaded the tutorial in 0.6721 seconds#PythonTimerFunctions:ThreeWaystoMonitorYourCode[ ... ]
That’s it! You’ve covered the basics of timing your own Python code. In the rest of the tutorial, you’ll learn how you can wrap a Python timer into a class, a context manager, and a decorator to make it more consistent and convenient to use.
Look back at how you added the Python timer to the example above. Note that you need at least one variable (tic
) to store the state of the Python timer before you download the tutorial. After studying the code a little, you might also note that the three highlighted lines are added only for timing purposes! Now, you’ll create a class that does the same as your manual calls toperf_counter()
, but in a more readable and consistent manner.
Throughout this tutorial, you’ll create and updateTimer
, a class that you can use to time your code in several different ways. The final code with some additional features is also available onPyPI under the namecodetiming
. You can install this on your system like so:
$python-mpipinstallcodetiming
You can find more information aboutcodetiming
later on in this tutorial, in the section namedThe Python Timer Code.
Classes are the main building blocks ofobject-oriented programming. Aclass is essentially a template that you can use to createobjects. While Python doesn’t force you to program in an object-oriented manner, classes are everywhere in the language. For quick proof, investigate thetime
module:
>>>importtime>>>type(time)<class 'module'>>>>time.__class__<class 'module'>
type()
returns the type of an object. Here you can see that modules are, in fact, objects created from amodule
class. You can use the special attribute.__class__
to get access to the class that defines an object. In fact, almost everything in Python is a class:
>>>type(3)<class 'int'>>>>type(None)<class 'NoneType'>>>>type(print)<class 'builtin_function_or_method'>>>>type(type)<class 'type'>
In Python, classes are great when you need to model something that needs to keep track of a particular state. In general, a class is a collection of properties, calledattributes, and behaviors, calledmethods. For more background on classes and object-oriented programming, check outPython Classes: The Power of Object-Oriented Programming,Object-Oriented Programming (OOP) in Python or theofficial documentation.
Classes are good for trackingstate. In aTimer
class, you want to keep track of when a timer starts and how much time has passed since then. For the first implementation ofTimer
, you’ll add a._start_time
attribute, as well as.start()
and.stop()
methods. Add the following code to a file namedtimer.py
:
timer.py
1importtime 2 3classTimerError(Exception): 4"""A custom exception used to report errors in use of Timer class""" 5 6classTimer: 7def__init__(self): 8self._start_time=None 910defstart(self):11"""Start a new timer"""12ifself._start_timeisnotNone:13raiseTimerError(f"Timer is running. Use .stop() to stop it")1415self._start_time=time.perf_counter()1617defstop(self):18"""Stop the timer, and report the elapsed time"""19ifself._start_timeisNone:20raiseTimerError(f"Timer is not running. Use .start() to start it")2122elapsed_time=time.perf_counter()-self._start_time23self._start_time=None24print(f"Elapsed time:{elapsed_time:0.4f} seconds")
A few different things are happening here, so take a moment to walk through the code step by step.
In line 3, you define aTimerError
class. The(Exception)
notation means thatTimerError
inherits from another class calledException
. Python uses this built-in class for error handling. You don’t need to add any attributes or methods toTimerError
, but having a custom error will give you more flexibility to handle problems insideTimer
. For more information, check outPython Exceptions: An Introduction.
The definition ofTimer
itself starts on line 6. When you first create orinstantiate an object from a class, your code calls.__init__()
, one of Python’sspecial methods. In this first version ofTimer
, you only initialize the._start_time
attribute, which you’ll use to track the state of your Python timer. It has the valueNone
when the timer isn’t running. Once the timer is running,._start_time
keeps track of when the timer started.
Note: Theunderscore (_
) prefix of._start_time
is a Python convention. It signals that._start_time
is an internal attribute that users of theTimer
class shouldn’t manipulate.
When you call.start()
to start a new Python timer, you first check that the timer isn’t already running. Then you store the current value ofperf_counter()
in._start_time
.
On the other hand, when you call.stop()
, you first check that the Python timer is running. If it is, then you calculate the elapsed time as the difference between the current value ofperf_counter()
and the one that you stored in._start_time
. Finally, you reset._start_time
so that the timer can be restarted, and print the elapsed time.
Here’s how you useTimer
:
>>>fromtimerimportTimer>>>t=Timer()>>>t.start()>>>t.stop()# A few seconds laterElapsed time: 3.8191 seconds
Compare this to theearlier example where you usedperf_counter()
directly. The structure of the code is fairly similar, but now the code is clearer, and this is one of the benefits of using classes. By carefully choosing your class, method, and attribute names, you can make your code very descriptive!
Now applyTimer
tolatest_tutorial.py
. You only need to make a few changes to your previous code:
latest_tutorial.py
fromtimerimportTimerfromreaderimportfeeddefmain():"""Print the latest tutorial from Real Python"""t=Timer()t.start()tutorial=feed.get_article(0)t.stop()print(tutorial)if__name__=="__main__":main()
Notice that the code is very similar to what you used earlier. In addition to making the code more readable,Timer
takes care of printing the elapsed time to the console, which makes the logging of time spent more consistent. When you run the code, you’ll get pretty much the same output:
$pythonlatest_tutorial.pyElapsed time: 0.6462 seconds#PythonTimerFunctions:ThreeWaystoMonitorYourCode[ ... ]
Printing the elapsed time fromTimer
may be consistent, but it seems that this approach is not very flexible. In the next section, you’ll see how to customize your class.
So far, you’ve learned that classes are suitable for when you want to encapsulate state and ensure consistent behavior in your code. In this section, you’ll add more convenience and flexibility to your Python timer:
First, see how you can customize the text used to report the time spent. In the previous code, the textf"Elapsed time: {elapsed_time:0.4f} seconds"
is hard-coded into.stop()
. You can add flexibility to classes usinginstance variables, whose values are normally passed as arguments to.__init__()
and stored asself
attributes. For convenience, you can also provide reasonable default values.
To add.text
as aTimer
instance variable, you’ll do something like this intimer.py
:
timer.py
def__init__(self,text="Elapsed time:{:0.4f} seconds"):self._start_time=Noneself.text=text
Note that the default text,"Elapsed time: {:0.4f} seconds"
, is given as a regular string, not as an f-string. You can’t use an f-string here because f-strings evaluate immediately, and when you instantiateTimer
, your code hasn’t yet calculated the elapsed time.
Note: If you want to use an f-string to specify.text
, then you need to use double curly braces to escape the curly braces that the actual elapsed time will replace.
One example would bef"Finished {task} in {{:0.4f}} seconds"
. If the value oftask
is"reading"
, then this f-string would be evaluated as"Finished reading in {:0.4f} seconds"
.
In.stop()
, you use.text
as a template and.format()
to populate the template:
timer.py
defstop(self):"""Stop the timer, and report the elapsed time"""ifself._start_timeisNone:raiseTimerError(f"Timer is not running. Use .start() to start it")elapsed_time=time.perf_counter()-self._start_timeself._start_time=Noneprint(self.text.format(elapsed_time))
After this update totimer.py
, you can change the text as follows:
>>>fromtimerimportTimer>>>t=Timer(text="You waited{:.1f} seconds")>>>t.start()>>>t.stop()# A few seconds laterYou waited 4.1 seconds
Next, assume that you don’t just want to print a message to the console. Maybe you want to save your time measurements so that you can store them in a database. You can do this by returning the value ofelapsed_time
from.stop()
. Then, the calling code can choose to either ignore that return value or save it for later processing.
Perhaps you want to integrateTimer
into yourlogging routines. To support logging or other outputs fromTimer
, you need to change the call toprint()
so that the user can supply their own logging function. This can be done similarly to how you customized the text earlier:
timer.py
1# ... 2 3classTimer: 4def__init__( 5self, 6text="Elapsed time:{:0.4f} seconds", 7logger=print 8): 9self._start_time=None10self.text=text11self.logger=logger1213# Other methods are unchanged1415defstop(self):16"""Stop the timer, and report the elapsed time"""17ifself._start_timeisNone:18raiseTimerError(f"Timer is not running. Use .start() to start it")1920elapsed_time=time.perf_counter()-self._start_time21self._start_time=None2223ifself.logger:24self.logger(self.text.format(elapsed_time))2526returnelapsed_time
Instead of usingprint()
directly, you create another instance variable in line 11,self.logger
, that should refer to a function that takes a string as an argument. In addition toprint()
, you can use functions likelogging.info()
or.write()
onfile objects. Also note theif
test in line 23, which allows you to turn off printing completely by passinglogger=None
.
Here are two examples that show the new functionality in action:
>>>fromtimerimportTimer>>>importlogging>>>t=Timer(logger=logging.warning)>>>t.start()>>>t.stop()# A few seconds laterWARNING:root:Elapsed time: 3.1610 seconds3.1609658249999484>>>t=Timer(logger=None)>>>t.start()>>>value=t.stop()# A few seconds later>>>value4.710851433001153
When you run these examples in an interactive shell, Python prints the return value automatically.
The third improvement that you’ll add is the ability to accumulatetime measurements. You may want to do this, for example, when you’re calling a slow function in a loop. You’ll add a bit more functionality in the form of named timers with adictionary that keeps track of every Python timer in your code.
Assume that you’re expandinglatest_tutorial.py
to alatest_tutorials.py
script that downloads and prints the ten latest tutorials from Real Python. The following is one possible implementation:
latest_tutorials.py
fromtimerimportTimerfromreaderimportfeeddefmain():"""Print the 10 latest tutorials from Real Python"""t=Timer(text="Downloaded 10 tutorials in{:0.2f} seconds")t.start()fortutorial_numinrange(10):tutorial=feed.get_article(tutorial_num)print(tutorial)t.stop()if__name__=="__main__":main()
The code loops over the numbers from 0 to 9 and uses those as offset arguments tofeed.get_article()
. When you run the script, you’ll print a lot of information to your console:
$pythonlatest_tutorials.py#PythonTimerFunctions:ThreeWaystoMonitorYourCode[ ... The text of the tutorials ... ]Downloaded 10 tutorials in 0.67 seconds
One subtle issue with this code is that you’re measuring not only the time it takes to download the tutorials, but also the time Python spends printing the tutorials to your screen. This might not be that important because the time spent printing should be negligible compared to the time spent downloading. Still, it would be good to have a way to precisely time what you’re after in these kinds of situations.
Note: The time spent downloading ten tutorials is about the same as the time spent downloading one tutorial. This isn’t a bug in your code! Instead,reader
caches the Real Python feed the first timeget_article()
is called, and reuses the information on later invocations.
There are several ways that you can work around this without changing the current implementation ofTimer.
However, supporting this use case will be quite useful, and you can do it with just a few lines of code.
First, you’ll introduce a dictionary called.timers
as aclass variable onTimer
, which means that all instances ofTimer
will share it. You implement it by defining it outside any methods:
classTimer:timers={}
Class variables can be accessed either directly on the class or through an instance of the class:
>>>fromtimerimportTimer>>>Timer.timers{}>>>t=Timer()>>>t.timers{}>>>Timer.timersist.timersTrue
In both cases, the code returns the same empty class dictionary.
Next, you’ll add optional names to your Python timer. You can use the name for two different purposes:
To add names to your Python timer, you need to make two more changes totimer.py
. First,Timer
should acceptname
as a parameter. Second, the elapsed time should be added to.timers
when a timer stops:
timer.py
# ...classTimer:timers={}def__init__(self,name=None,text="Elapsed time:{:0.4f} seconds",logger=print,):self._start_time=Noneself.name=nameself.text=textself.logger=logger# Add new named timers to dictionary of timersifname:self.timers.setdefault(name,0)# Other methods are unchangeddefstop(self):"""Stop the timer, and report the elapsed time"""ifself._start_timeisNone:raiseTimerError(f"Timer is not running. Use .start() to start it")elapsed_time=time.perf_counter()-self._start_timeself._start_time=Noneifself.logger:self.logger(self.text.format(elapsed_time))ifself.name:self.timers[self.name]+=elapsed_timereturnelapsed_time
Note that you use.setdefault()
when adding the new Python timer to.timers
. This is a greatfeature that only sets the value ifname
isn’t already defined in the dictionary. Ifname
is already used in.timers
, then the value is left untouched. This allows you to accumulate several timers:
>>>fromtimerimportTimer>>>t=Timer("accumulate")>>>t.start()>>>t.stop()# A few seconds laterElapsed time: 3.7036 seconds3.703554293999332>>>t.start()>>>t.stop()# A few seconds laterElapsed time: 2.3449 seconds2.3448921170001995>>>Timer.timers{'accumulate': 6.0484464109995315}
You can now revisitlatest_tutorials.py
and make sure only the time spent on downloading the tutorials is measured:
latest_tutorials.py
fromtimerimportTimerfromreaderimportfeeddefmain():"""Print the 10 latest tutorials from Real Python"""t=Timer("download",logger=None)fortutorial_numinrange(10):t.start()tutorial=feed.get_article(tutorial_num)t.stop()print(tutorial)download_time=Timer.timers["download"]print(f"Downloaded 10 tutorials in{download_time:0.2f} seconds")if__name__=="__main__":main()
Rerunning the script will give output similar to earlier, although now you’re only timing the actual download of the tutorials:
$pythonlatest_tutorials.py#PythonTimerFunctions:ThreeWaystoMonitorYourCode[ ... The text of the tutorials ... ]Downloaded 10 tutorials in 0.65 seconds
The final improvement that you’ll make toTimer
is to make it more informative when you’re working with it interactively. Try the following:
>>>fromtimerimportTimer>>>t=Timer()>>>t<timer.Timer object at 0x7f0578804320>
That last line is the default way that Python represents objects. While you can glean some information from it, it’s usually not very useful. Instead, it would be nice to see information like the name ofTimer
, or how it’ll report on the timings.
In Python 3.7,data classes were added to the standard library. These provide several conveniences to your classes, including a more informative representation string.
You convert your Python timer to a data class using the@dataclass
decorator. You’ll learn more about decoratorslater in this tutorial. For now, you can think of this as a notation that tells Python thatTimer
is a data class:
1importtime 2fromdataclassesimportdataclass,field 3fromtypingimportAny,ClassVar 4 5# ... 6 7@dataclass 8classTimer: 9timers:ClassVar={}10name:Any=None11text:Any="Elapsed time:{:0.4f} seconds"12logger:Any=print13_start_time:Any=field(default=None,init=False,repr=False)1415def__post_init__(self):16"""Initialization: add timer to dict of timers"""17ifself.name:18self.timers.setdefault(self.name,0)1920# The rest of the code is unchanged
This code replaces your earlier.__init__()
method. Note how data classes use syntax that looks similar to the class variable syntax that you saw earlier for defining all variables. In fact,.__init__()
is created automatically for data classes, based onannotated variables in the definition of the class.
You need to annotate your variables to use a data class. You can use this annotation to addtype hints to your code. If you don’t want to use type hints, then you can instead annotate all variables withAny
, just like you did above. You’ll soon learn how to add actual type hints to your data class.
Here are a few notes about theTimer
data class:
Line 7: The@dataclass
decorator definesTimer
as a data class.
Line 9: The specialClassVar
annotation is necessary for data classes to specify that.timers
is a class variable.
Lines 10 to 12:.name
,.text
, and.logger
will be defined as attributes onTimer
, whose values can be specified when creatingTimer
instances. They all have the given default values.
Line 13: Recall that._start_time
is a special attribute that’s used to keep track of the state of the Python timer, but it should be hidden from the user. Usingdataclasses.field()
, you say that._start_time
should be removed from.__init__()
and the representation ofTimer
.
Lines 15 to 18: You can use the special.__post_init__()
method for any initialization that you need to do apart from setting the instance attributes. Here, you use it to add named timers to.timers
.
Your newTimer
data class works just like your previous regular class, except that it now has a nice representation:
>>>fromtimerimportTimer>>>t=Timer()>>>tTimer(name=None, text='Elapsed time: {:0.4f} seconds', logger=<built-in function print>)>>>t.start()>>>t.stop()# A few seconds laterElapsed time: 6.7197 seconds6.719705373998295
Now you have a pretty neat version ofTimer
that’s consistent, flexible, convenient, and informative! You can apply many of the improvements that you’ve made in this section to other types of classes in your projects as well.
Before ending this section, revisit the complete source code ofTimer
as it currently stands. You’ll notice the addition oftype hints to the code for extra documentation:
timer.py
fromdataclassesimportdataclass,fieldimporttimefromtypingimportCallable,ClassVar,Dict,OptionalclassTimerError(Exception):"""A custom exception used to report errors in use of Timer class"""@dataclassclassTimer:timers:ClassVar[Dict[str,float]]={}name:Optional[str]=Nonetext:str="Elapsed time:{:0.4f} seconds"logger:Optional[Callable[[str],None]]=print_start_time:Optional[float]=field(default=None,init=False,repr=False)def__post_init__(self)->None:"""Add timer to dict of timers after initialization"""ifself.nameisnotNone:self.timers.setdefault(self.name,0)defstart(self)->None:"""Start a new timer"""ifself._start_timeisnotNone:raiseTimerError(f"Timer is running. Use .stop() to stop it")self._start_time=time.perf_counter()defstop(self)->float:"""Stop the timer, and report the elapsed time"""ifself._start_timeisNone:raiseTimerError(f"Timer is not running. Use .start() to start it")# Calculate elapsed timeelapsed_time=time.perf_counter()-self._start_timeself._start_time=None# Report elapsed timeifself.logger:self.logger(self.text.format(elapsed_time))ifself.name:self.timers[self.name]+=elapsed_timereturnelapsed_time
Using a class to create a Python timer has several benefits:
This class is very flexible, and you can use it in almost any situation where you want to monitor the time it takes for code to run. However, in the next sections, you’ll learn about using context managers and decorators, which will be more convenient for timing code blocks and functions.
Your PythonTimer
class has come a long way! Compared with thefirst Python timer you created, your code has gotten quite powerful. However, there’s still a bit of boilerplate code necessary to use yourTimer
:
.start()
before the code block that you want to time..stop()
after the code block.Luckily, Python has a unique construct for calling functions before and after a block of code: thecontext manager. In this section, you’ll learn whatcontext managers and Python’swith
statement are, and how you can create your own. Then you’ll expandTimer
so that it can work as a context manager as well. Finally, you’ll see how usingTimer
as a context manager can simplify your code.
Context managers have been a part of Python for a long time. They were introduced byPEP 343 in 2005, and first implemented in Python 2.5. You can recognize context managers in code by the use of thewith
keyword:
withEXPRESSIONasVARIABLE:BLOCK
EXPRESSION
is some Python expression that returns a context manager. The context manager is optionally bound to the nameVARIABLE
. Finally,BLOCK
is any regular Python code block. The context manager will guarantee that your program calls some code beforeBLOCK
and some other code afterBLOCK
executes. The latter will happen, even ifBLOCK
raises an exception.
The most common use of context managers is probably handling different resources, like files, locks, and database connections. The context manager is then used to free and clean up the resource after you’ve used it. The following example reveals the fundamental structure oftimer.py
by only printing lines that contain a colon. More importantly, it shows the common idiom foropening a file in Python:
>>>withopen("timer.py")asfp:...print("".join(lnforlninfpif":"inln))...class TimerError(Exception):class Timer: timers: ClassVar[Dict[str, float]] = {} name: Optional[str] = None text: str = "Elapsed time: {:0.4f} seconds" logger: Optional[Callable[[str], None]] = print _start_time: Optional[float] = field(default=None, init=False, repr=False) def __post_init__(self) -> None: if self.name is not None: def start(self) -> None: if self._start_time is not None: def stop(self) -> float: if self._start_time is None: if self.logger: if self.name:
Note thatfp
, the file pointer, is never explicitly closed because you usedopen()
as a context manager. You can confirm thatfp
has closed automatically:
>>>fp.closedTrue
In this example,open("timer.py")
is an expression that returns a context manager. That context manager is bound to the namefp
. The context manager is in effect during the execution ofprint()
. This one-line code block executes in the context offp
.
What does it mean thatfp
is a context manager? Technically, it means thatfp
implements thecontext manager protocol. There are many differentprotocols underlying the Python language. You can think of a protocol as a contract that states what specific methods your code must implement.
Thecontext manager protocol consists of two methods:
.__enter__()
when entering the context related to the context manager..__exit__()
when exiting the context related to the context manager.In other words, to create a context manager yourself, you need to write a class that implements.__enter__()
and.__exit__()
. No more, no less. Try aHello, World! context manager example:
# greeter.pyclassGreeter:def__init__(self,name):self.name=namedef__enter__(self):print(f"Hello{self.name}")returnselfdef__exit__(self,exc_type,exc_value,exc_tb):print(f"See you later,{self.name}")
Greeter
is a context manager because it implements the context manager protocol. You can use it like this:
>>>fromgreeterimportGreeter>>>withGreeter("Akshay"):...print("Doing stuff ...")...Hello AkshayDoing stuff ...See you later, Akshay
First, note how.__enter__()
is called before you’re doing stuff, while.__exit__()
is called after. In this simplified example, you’re not referencing the context manager. In such cases, you don’t need to give the context manager a name withas
.
Next, notice how.__enter__()
returnsself
. The return value of.__enter__()
is bound byas
. You usually want to returnself
from.__enter__()
when creating context managers. You can use that return value as follows:
>>>fromgreeterimportGreeter>>>withGreeter("Bethan")asgrt:...print(f"{grt.name} is doing stuff ...")...Hello BethanBethan is doing stuff ...See you later, Bethan
Finally,.__exit__()
takes three arguments:exc_type
,exc_value
, andexc_tb
. These are used for error handling within the context manager, and they mirror thereturn values ofsys.exc_info()
.
If an exception happens while the block is being executed, then your code calls.__exit__()
with the type of the exception, an exception instance, and atraceback object. Often, you can ignore these in your context manager, in which case.__exit__()
is called before the exception is reraised:
>>>fromgreeterimportGreeter>>>withGreeter("Rascal")asgrt:...print(f"{grt.age} does not exist")...Hello RascalSee you later, RascalTraceback (most recent call last): File"<stdin>", line2, in<module>AttributeError:'Greeter' object has no attribute 'age'
You can see that"See you later, Rascal"
is printed, even though there is an error in the code.
Now you know what context managers are and how you can create your own. If you want to dive deeper, then check outcontextlib
in the standard library. It includes convenient ways for defining new context managers, as well as ready-made context managers that you can use toclose objects,suppress errors, or evendo nothing! For even more information, check outContext Managers and Python’swith
Statement.
You’ve seen how context managers work in general, but how can they help with timing code? If you can run certain functions before and after a block of code, then you can simplify how your Python timer works. So far, you’ve needed to call.start()
and.stop()
explicitly when timing your code, but a context manager can do this automatically.
Again, forTimer
to work as a context manager, it needs to adhere to the context manager protocol. In other words, it must implement.__enter__()
and.__exit__()
to start and stop the Python timer. All the necessary functionality is already available, so there’s not much new code you need to write. Just add the following methods to yourTimer
class:
timer.py
# ...@dataclassclassTimer:# The rest of the code is unchangeddef__enter__(self):"""Start a new timer as a context manager"""self.start()returnselfdef__exit__(self,*exc_info):"""Stop the context manager timer"""self.stop()
Timer
is now a context manager. The important part of the implementation is that.__enter__()
calls.start()
to start a Python timer when the context is entered, and.__exit__()
uses.stop()
to stop the Python timer when the code leaves the context. Try it out:
>>>fromtimerimportTimer>>>importtime>>>withTimer():...time.sleep(0.7)...Elapsed time: 0.7012 seconds
You should also note two more subtle details:
.__enter__()
returnsself
, theTimer
instance, which allows the user to bind theTimer
instance to a variable usingas
. For example,with Timer() as t:
will create the variablet
pointing to theTimer
object.
.__exit__()
expects three arguments with information about any exception that occurred during the execution of the context. In your code, these arguments are packed into a tuple calledexc_info
and then ignored, which means thatTimer
won’t attempt any exception handling.
.__exit__()
doesn’t do any error handling in this case. Still, one of the great features of context managers is that they’re guaranteed to call.__exit__()
, no matter how the context exits. In the following example, you purposely create an error by dividing by zero:
>>>fromtimerimportTimer>>>withTimer():...fornuminrange(-3,3):...print(f"1 /{num} ={1/num:.3f}")...1 / -3 = -0.3331 / -2 = -0.5001 / -1 = -1.000Elapsed time: 0.0001 secondsTraceback (most recent call last): File"<stdin>", line3, in<module>ZeroDivisionError:division by zero
Note thatTimer
prints out the elapsed time, even though the code crashed. It’s possible to inspect and suppress errors in.__exit__()
. See thedocumentation for more information.
Now you’ll learn how to use theTimer
context manager to time the download of Real Python tutorials. Recall how you usedTimer
earlier:
latest_tutorial.py
fromtimerimportTimerfromreaderimportfeeddefmain():"""Print the latest tutorial from Real Python"""t=Timer()t.start()tutorial=feed.get_article(0)t.stop()print(tutorial)if__name__=="__main__":main()
You’re timing the call tofeed.get_article()
. You can use the context manager to make the code shorter, simpler, and more readable:
latest_tutorial.py
fromtimerimportTimerfromreaderimportfeeddefmain():"""Print the latest tutorial from Real Python"""withTimer():tutorial=feed.get_article(0)print(tutorial)if__name__=="__main__":main()
This code does virtually the same as the code above. The main difference is that you don’t define the extraneous variablet
, which keeps yournamespace cleaner.
Running the script should give a familiar result:
$pythonlatest_tutorial.pyElapsed time: 0.71 seconds#PythonTimerFunctions:ThreeWaystoMonitorYourCode[ ... ]
There are a few advantages to adding context manager capabilities to your Python timer class:
UsingTimer
as a context manager is almost as flexible as using.start()
and.stop()
directly, while it has less boilerplate code. In the next section, you’ll learn how you can useTimer
as a decorator as well. This will make it easier to monitor the runtime of complete functions.
YourTimer
class is now very versatile. However, there’s one use case where you could streamline it even further. Say that you want to track the time spent inside one given function in your codebase. Using a context manager, you have essentially two different options:
UseTimer
every time you call the function:
withTimer("some_name"):do_something()
If you calldo_something()
in many places, then this will become cumbersome and hard to maintain.
Wrap the code in your function inside a context manager:
defdo_something():withTimer("some_name"):...
TheTimer
only needs to be added in one place, but this adds a level of indentation to the whole definition ofdo_something()
.
A better solution is to useTimer
as adecorator. Decorators are powerful constructs that you use to modify the behavior of functions and classes. In this section, you’ll learn a little about how decorators work, how you can extendTimer
to be a decorator, and how that will simplify timing functions. For a more in-depth explanation of decorators, seePrimer on Python Decorators.
Adecorator is a function that wraps another function to modify its behavior. This technique is possible because functions arefirst-class objects in Python. In other words, functions can be assigned to variables and used as arguments to other functions, just like any other object. This gives you a lot of flexibility and is the basis for several of Python’s most powerful features.
As a first example, you’ll create a decorator that does nothing:
defturn_off(func):returnlambda*args,**kwargs:None
First, note thatturn_off()
is just a regular function. What makes this a decorator is that it takes a function as its only argument and returns a function. You can useturn_off()
to modify other functions, like this:
>>>print("Hello")Hello>>>print=turn_off(print)>>>print("Hush")>>># Nothing is printed
The lineprint = turn_off(print)
decorates the print statement with theturn_off()
decorator. Effectively, it replacesprint()
withlambda *args, **kwargs: None
returned byturn_off()
. Thelambda statement represents an anonymous function that does nothing except returnNone
.
To define more interesting decorators, you need to know aboutinner functions. Aninner function is a function that’s defined inside another function. One common use of inner functions is to create function factories:
defcreate_multiplier(factor):defmultiplier(num):returnfactor*numreturnmultiplier
multiplier()
is an inner function, defined insidecreate_multiplier()
. Note that you have access tofactor
insidemultiplier()
, whilemultiplier()
isn’t defined outsidecreate_multiplier()
:
>>>multiplierTraceback (most recent call last): File"<stdin>", line1, in<module>NameError:name 'multiplier' is not defined
Instead you usecreate_multiplier()
to create new multiplier functions, each based on a different factor:
>>>double=create_multiplier(factor=2)>>>double(3)6>>>quadruple=create_multiplier(factor=4)>>>quadruple(7)28
Similarly, you can use inner functions to create decorators. Remember, a decorator is a function that returns a function:
1deftriple(func): 2defwrapper_triple(*args,**kwargs): 3print(f"Tripled{func.__name__!r}") 4value=func(*args,**kwargs) 5returnvalue*3 6returnwrapper_triple
triple()
is a decorator, because it’s a function that expects a function,func()
, as its only argument and returns another function,wrapper_triple()
. Note the structure oftriple()
itself:
triple()
and expects a function as an argument.wrapper_triple()
.wrapper_triple()
.This pattern is prevalent for defining decorators. The interesting parts are those happening inside the inner function:
wrapper_triple()
. This function will replace whichever functiontriple()
decorates. The parameters are*args
and**kwargs
, which collect whichever positional and keyword arguments you pass to the function. This gives you the flexibility to usetriple()
on any function.triple()
has been applied to it.func()
, the function thattriple()
has decorated. It passes on all arguments passed towrapper_triple()
.func()
and returns it.Try it out!knock()
is a function that returns the wordPenny
. See what happens if it’s tripled:
>>>defknock():...return"Penny! "...>>>knock=triple(knock)>>>result=knock()Tripled 'knock'>>>result'Penny! Penny! Penny! '
Multiplying a text string by a number is a form of repetition, soPenny
repeats three times. The decoration happens atknock = triple(knock)
.
It feels a bit clunky to keep repeatingknock
. Instead,PEP 318 introduced a more convenient syntax for applying decorators. The following definition ofknock()
does the same as the one above:
>>>@triple...defknock():...return"Penny! "...>>>result=knock()Tripled 'knock'>>>result'Penny! Penny! Penny! '
The@
symbol is used to apply decorators. In this case,@triple
means thattriple()
is applied to the function defined just after it.
One of the few decorators defined in the standard library is@functools.wraps
. This one is quite helpful when defining your own decorators. Because decorators effectively replace one function with another, they create a subtle issue with your functions:
>>>knock<function triple.<locals>.wrapper_triple at 0x7fa3bfe5dd90>
@triple
decoratesknock()
, which is then replaced by thewrapper_triple()
inner function, as the output above confirms. This will also replace the name,docstring, and other metadata. Often, this won’t have much effect, but it can make introspection difficult.
Sometimes, decorated functions must have correct metadata.@functools.wraps
fixes exactly this issue:
importfunctoolsdeftriple(func):@functools.wraps(func)defwrapper_triple(*args,**kwargs):print(f"Tripled{func.__name__!r}")value=func(*args,**kwargs)returnvalue*3returnwrapper_triple
With this new definition of@triple
, metadata is preserved:
>>>@triple...defknock():...return"Penny! "...>>>knock<function knock at 0x7fa3bfe5df28>
Note thatknock()
now keeps its proper name, even after being decorated. It’s good form to use@functools.wraps
whenever you define a decorator. A blueprint that you can use for most of your decorators is the following:
importfunctoolsdefdecorator(func):@functools.wraps(func)defwrapper_decorator(*args,**kwargs):# Do something beforevalue=func(*args,**kwargs)# Do something afterreturnvaluereturnwrapper_decorator
To see more examples of how to define decorators, check out the examples listed inPrimer on Python Decorators.
In this section, you’ll learn how to extend your Python timer so that you can use it as a decorator as well. However, as a first exercise, you’ll create a Python timer decorator from scratch.
Based on the blueprint above, you only need to decide what to do before and after you call the decorated function. This is similar to the considerations about what to do when entering and exiting the context manager. You want to start a Python timer before calling the decorated function, and stop the Python timer after the call finishes. You can define a@timer
decorator as follows:
importfunctoolsimporttimedeftimer(func):@functools.wraps(func)defwrapper_timer(*args,**kwargs):tic=time.perf_counter()value=func(*args,**kwargs)toc=time.perf_counter()elapsed_time=toc-ticprint(f"Elapsed time:{elapsed_time:0.4f} seconds")returnvaluereturnwrapper_timer
Note how muchwrapper_timer()
resembles the earlier pattern that you established for timing Python code. You can apply@timer
as follows:
>>>@timer...deflatest_tutorial():...tutorial=feed.get_article(0)...print(tutorial)...>>>latest_tutorial()# Python Timer Functions: Three Ways to Monitor Your Code[ ... ]Elapsed time: 0.5414 seconds
Recall that you can also apply a decorator to a previously defined function:
>>>feed.get_article=timer(feed.get_article)
Because@
applies when functions are defined, you need to use the more basic form in these cases. One advantage of using a decorator is that you only need to apply it once, and it’ll time the function every time:
>>>tutorial=feed.get_article(0)Elapsed time: 0.5512 seconds
@timer
does the job. However, in a sense, you’re back to square one, since@timer
doesn’t have any of the flexibility or convenience ofTimer
. Can you also make yourTimer
class act like a decorator?
So far, you’ve used decorators as functions applied to other functions, but that’s not entirely correct. Decorators must becallables. There are manycallable types in Python. You can make your own objects callable by defining the special.__call__()
method in their class. The following function and class behave similarly:
>>>defsquare(num):...returnnum**2...>>>square(4)16>>>classSquarer:...def__call__(self,num):...returnnum**2...>>>square=Squarer()>>>square(4)16
Here,square
is an instance that is callable and can square numbers, just like thesquare()
function in the first example.
This gives you a way of adding decorator capabilities to the existingTimer
class:
timer.py
importfunctools# ...@dataclassclassTimer:# The rest of the code is unchangeddef__call__(self,func):"""Support using Timer as a decorator"""@functools.wraps(func)defwrapper_timer(*args,**kwargs):withself:returnfunc(*args,**kwargs)returnwrapper_timer
.__call__()
uses the fact thatTimer
is already a context manager to take advantage of the conveniences that you’ve already defined there. Make sure you alsoimportfunctools
at the top oftimer.py
.
You can now useTimer
as a decorator:
>>>@Timer(text="Downloaded the tutorial in{:.2f} seconds")...deflatest_tutorial():...tutorial=feed.get_article(0)...print(tutorial)...>>>latest_tutorial()# Python Timer Functions: Three Ways to Monitor Your Code[ ... ]Downloaded the tutorial in 0.72 seconds
Before rounding out this section, know that there’s a more straightforward way of turning your Python timer into a decorator. You’ve already seen some of the similarities between context managers and decorators. They’re both typically used to do something before and after executing some given code.
Based on these similarities, there’s amixin class defined in the standard library calledContextDecorator
. You can add decorator abilities to your context manager classes simply by inheritingContextDecorator
:
fromcontextlibimportContextDecorator# ...@dataclassclassTimer(ContextDecorator):# Implementation of Timer is unchanged
When you useContextDecorator
this way, there’s no need to implement.__call__()
yourself, so you can safely delete it from theTimer
class.
Next up, you’ll redo thelatest_tutorial.py
example one last time, using the Python timer as a decorator:
latest_tutorial.py
1fromtimerimportTimer 2fromreaderimportfeed 3 4@Timer() 5defmain(): 6"""Print the latest tutorial from Real Python""" 7tutorial=feed.get_article(0) 8print(tutorial) 910if__name__=="__main__":11main()
If you compare this implementation with theoriginal implementation without any timing, then you’ll notice that the only differences are the import ofTimer
on line 1 and the application of@Timer()
on line 4. A significant advantage of using decorators is that they’re usually straightforward to apply, as you see here.
However, the decorator still applies to the whole function. This means that your code is taking into account the time it takes to print the tutorial, in addition to the time it takes to download. Run the script one final time:
$pythonlatest_tutorial.py#PythonTimerFunctions:ThreeWaystoMonitorYourCode[ ... ]Elapsed time: 0.69 seconds
The location of the elapsed time output is a telltale sign that your code is considering the time it takes to print as well. As you see here, your code prints the elapsed timeafter the tutorial.
When you useTimer
as a decorator, you’ll see similar advantages as you did with context managers:
However, decorators are not as flexible as context managers. You can only apply them to complete functions. It’s possible to add decorators to already defined functions, but this is a bit clunky and less common.
You can expand the code block below to view the final source code for your Python timer:
timer.py
importtimefromcontextlibimportContextDecoratorfromdataclassesimportdataclass,fieldfromtypingimportAny,Callable,ClassVar,Dict,OptionalclassTimerError(Exception):"""A custom exception used to report errors in use of Timer class"""@dataclassclassTimer(ContextDecorator):"""Time your code using a class, context manager, or decorator"""timers:ClassVar[Dict[str,float]]={}name:Optional[str]=Nonetext:str="Elapsed time:{:0.4f} seconds"logger:Optional[Callable[[str],None]]=print_start_time:Optional[float]=field(default=None,init=False,repr=False)def__post_init__(self)->None:"""Initialization: add timer to dict of timers"""ifself.name:self.timers.setdefault(self.name,0)defstart(self)->None:"""Start a new timer"""ifself._start_timeisnotNone:raiseTimerError(f"Timer is running. Use .stop() to stop it")self._start_time=time.perf_counter()defstop(self)->float:"""Stop the timer, and report the elapsed time"""ifself._start_timeisNone:raiseTimerError(f"Timer is not running. Use .start() to start it")# Calculate elapsed timeelapsed_time=time.perf_counter()-self._start_timeself._start_time=None# Report elapsed timeifself.logger:self.logger(self.text.format(elapsed_time))ifself.name:self.timers[self.name]+=elapsed_timereturnelapsed_timedef__enter__(self)->"Timer":"""Start a new timer as a context manager"""self.start()returnselfdef__exit__(self,*exc_info:Any)->None:"""Stop the context manager timer"""self.stop()
The code is also available in thecodetiming
repository on GitHub.
You can use the code yourself by saving it to a file namedtimer.py
and importing it into your program:
>>>fromtimerimportTimer
Timer
is also available onPyPI, so an even easier option is to install it usingpip
:
$python-mpipinstallcodetiming
Note that the package name on PyPI iscodetiming
. You’ll need to use this name both when you install the package and when you importTimer
:
>>>fromcodetimingimportTimer
Apart from the name andsome additional features,codetiming.Timer
works exactly astimer.Timer
. To summarize, you can useTimer
in three different ways:
As aclass:
t=Timer(name="class")t.start()# Do somethingt.stop()
As acontext manager:
withTimer(name="context manager"):# Do something
As adecorator:
@Timer(name="decorator")defstuff():# Do something
This kind of Python timer is mainly useful for monitoring the time that your code spends at individual key code blocks or functions. In the next section, you’ll get a quick overview of alternatives that you can use if you want optimize your code.
There are many options for timing your code with Python. In this tutorial, you’ve learned how to create a flexible and convenient class that you can use in several different ways. A quicksearch on PyPI shows that there are already many projects available that offer Python timer solutions.
In this section, you’ll first learn more about the different functions available in the standard library for measuring time, including whyperf_counter()
is preferable. Then, you’ll explore alternatives for optimizing your code, for whichTimer
is not well-suited.
You’ve been usingperf_counter()
throughout this tutorial to do the actual time measurements, but Python’stime
library comes with several other functions that also measure time. Here are some alternatives:
One reason for having several functions is that Python represents time as afloat
.Floating-point numbers are inaccurate by nature. You may have seen results like these before:
>>>0.1+0.1+0.10.30000000000000004>>>0.1+0.1+0.1==0.3False
Python’sfloat
follows theIEEE 754 Standard for Floating-Point Arithmetic, which tries to represent all floating-point numbers in 64 bits. Because there are infinitely many floating-point numbers, you can’t express them all with a finite number of bits.
IEEE 754 prescribes a system where the density of numbers that you can represent varies. The closer you are to one, the more numbers you can represent. For larger numbers, there’s morespace between the numbers that you can express. This has some consequences when you use afloat
to represent time.
Considertime()
. The main purpose of this function is to represent the actual time right now. It does this as the number of seconds since a given point in time, called theepoch. The number returned bytime()
is quite big, which means that there are fewer numbers available, and the resolution suffers. Specifically,time()
is not able to measurenanosecond differences:
>>>importtime>>>t=time.time()>>>t1564342757.0654016>>>t+1e-91564342757.0654016>>>t==t+1e-9True
A nanosecond is one-billionth of a second. Note that adding a nanosecond tot
doesn’t affect the result.perf_counter()
, on the other hand, uses some undefined point in time as its epoch, allowing it to work with smaller numbers and therefore obtain a better resolution:
>>>importtime>>>p=time.perf_counter()>>>p11370.015653846>>>p+1e-911370.015653847>>>p==p+1e-9False
Here, you notice that adding a nanosecond top
actually affects the outcome. For more information about how to work withtime()
, seeA Beginner’s Guide to the Python time Module.
The challenges with representing time as afloat
are well known, so Python 3.7 introduced a new option. Eachtime
measurement function now has a corresponding_ns
function that returns the number of nanoseconds as anint
instead of the number of seconds as afloat
. For instance,time()
now has a nanosecond counterpart calledtime_ns()
:
>>>importtime>>>time.time_ns()1564342792866601283
Integers are unbounded in Python, so this allowstime_ns()
to give nanosecond resolution for all eternity. Similarly,perf_counter_ns()
is a nanosecond variant ofperf_counter()
:
>>>importtime>>>time.perf_counter()13580.153084446>>>time.perf_counter_ns()13580765666638
Becauseperf_counter()
already provides nanosecond resolution, there are fewer advantages to usingperf_counter_ns()
.
Note:perf_counter_ns()
is only available in Python 3.7 and later. In this tutorial, you’ve usedperf_counter()
in yourTimer
class. That way, you can useTimer
on older Python versions as well.
For more information about the_ns
functions intime
, check outCool New Features in Python 3.7.
There are two functions intime
that do not measure the time spent sleeping. These areprocess_time()
andthread_time()
, which are useful in some settings. However, forTimer
, you typically want to measure the full time spent. The final function in the list above ismonotonic()
. The name alludes to this function being a monotonic timer, which is a Python timer that can never move backward.
All these functions are monotonic excepttime()
, which can go backward if the system time is adjusted. On some systems,monotonic()
is the same function asperf_counter()
, and you can use them interchangeably. However, this is not always the case. You can usetime.get_clock_info()
to get more information about a Python timer function:
>>>importtime>>>time.get_clock_info("monotonic")namespace(adjustable=False, implementation='clock_gettime(CLOCK_MONOTONIC)', monotonic=True, resolution=1e-09)>>>time.get_clock_info("perf_counter")namespace(adjustable=False, implementation='clock_gettime(CLOCK_MONOTONIC)', monotonic=True, resolution=1e-09)
The results could be different on your system.
PEP 418 describes some of the rationale behind introducing these functions. It includes the following short descriptions:
time.monotonic()
: timeout and scheduling, not affected by system clock updatestime.perf_counter()
: benchmarking, most precise clock for short periodtime.process_time()
: profiling, CPU time of the process (Source)
As you can tell,perf_counter()
is usually the best choice for your Python timer.
timeit
Say you’re trying to squeeze the last bit of performance out of your code, and you’re wondering about the most effective way to convert a list to a set. You want to compare usingset()
and the set literal,{...}
. You can use your Python timer for this:
>>>fromtimerimportTimer>>>numbers=[7,6,1,4,1,8,0,6]>>>withTimer(text="{:.8f}"):...set(numbers)...{0, 1, 4, 6, 7, 8}0.00007373>>>withTimer(text="{:.8f}"):...{*numbers}...{0, 1, 4, 6, 7, 8}0.00006204
This test seems to indicate that the set literal might be slightly faster. However, these results are quite uncertain, and if you rerun the code, you might get wildly different results. That’s because you’re only trying the code once. You could, for instance, get unlucky and run the script just as your computer is becoming busy with other tasks.
A better way is to use thetimeit
standard library. It’s designed precisely to measure the execution time of small code snippets. While you can import and calltimeit.timeit()
from Python as a regular function, it’s usually more convenient to use thecommand-line interface. You can time the two variants as follows:
$python-mtimeit--setup"nums = [7, 6, 1, 4, 1, 8, 0, 6]""set(nums)"2000000 loops, best of 5: 163 nsec per loop$python-mtimeit--setup"nums = [7, 6, 1, 4, 1, 8, 0, 6]""{*nums}"2000000 loops, best of 5: 121 nsec per loop
timeit
automatically calls your code many times to average out noisy measurements. The results fromtimeit
confirm that the set literal is faster thanset()
.
Note: Be careful when you’re usingtimeit
on code that candownload files or access databases. Sincetimeit
automatically calls your program several times, you could unintentionally end up spamming the server with requests!
Finally, theIPython interactive shell and theJupyter Notebook have extra support for this functionality with the%timeit
magic command:
In [1]:numbers=[7,6,1,4,1,8,0,6]In [2]:%timeit set(numbers)171 ns ± 0.748 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)In [3]:%timeit {*numbers}147 ns ± 2.62 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
Again, the measurements indicate that using a set literal is faster. In Jupyter Notebooks, you can also use the%%timeit
cell-magic to measure the time of running a whole cell.
timeit
is excellent for benchmarking a particular snippet of code. However, it would be very cumbersome to use it to check all parts of your program and locate which sections take the most time. Instead, you can use aprofiler.
cProfile
is a profiler that you can access at any time from the standard library. You can use it in several ways, although it’s usually most straightforward to use it as a command-line tool:
$python-mcProfile-olatest_tutorial.proflatest_tutorial.py
This command runslatest_tutorial.py
with profiling turned on. You save the output fromcProfile
inlatest_tutorial.prof
, as specified by the-o
option. The output data is in a binary format that needs a dedicated program to make sense of it. Again, Python has an option right in the standard library! Running thepstats
module on your.prof
file opens an interactive profile statistics browser:
$python-mpstatslatest_tutorial.profWelcome to the profile statistics browser.latest_tutorial.prof% helpDocumented commands (type help <topic>):========================================EOF add callees callers help quit read reverse sort stats strip
To usepstats
, you type commands at the prompt. Here you can see the integrated help system. Typically you’ll use thesort
andstats
commands. To get a cleaner output,strip
can be useful:
latest_tutorial.prof% striplatest_tutorial.prof% sort cumtimelatest_tutorial.prof% stats 10 1393801 function calls (1389027 primitive calls) in 0.586 seconds Ordered by: cumulative time List reduced from 1443 to 10 due to restriction <10> ncalls tottime percall cumtime percall filename:lineno(function) 144/1 0.001 0.000 0.586 0.586 {built-in method builtins.exec} 1 0.000 0.000 0.586 0.586 latest_tutorial.py:3(<module>) 1 0.000 0.000 0.521 0.521 contextlib.py:71(inner) 1 0.000 0.000 0.521 0.521 latest_tutorial.py:6(read_latest_tutorial) 1 0.000 0.000 0.521 0.521 feed.py:28(get_article) 1 0.000 0.000 0.469 0.469 feed.py:15(_feed) 1 0.000 0.000 0.469 0.469 feedparser.py:3817(parse) 1 0.000 0.000 0.271 0.271 expatreader.py:103(parse) 1 0.000 0.000 0.271 0.271 xmlreader.py:115(parse) 13 0.000 0.000 0.270 0.021 expatreader.py:206(feed)
This output shows that the total runtime was 0.586 seconds. It also lists the ten functions where your code spent most of its time. Here you’ve sorted by cumulative time (cumtime
), which means that your code counts time when the given function has called another function.
You can see that your code spends virtually all its time inside thelatest_tutorial
module, and in particular, insideread_latest_tutorial()
. While this might be useful confirmation of what you already know, it’s often more interesting to find where your code actually spends time.
The total time (tottime
) column indicates how much time your code spent inside a function, excluding time in sub-functions. You can see that none of the functions above really spend any time doing this. To find where the code spent most of its time, issue anothersort
command:
latest_tutorial.prof% sort tottimelatest_tutorial.prof% stats 10 1393801 function calls (1389027 primitive calls) in 0.586 seconds Ordered by: internal time List reduced from 1443 to 10 due to restriction <10> ncalls tottime percall cumtime percall filename:lineno(function) 59 0.091 0.002 0.091 0.002 {method 'read' of '_ssl._SSLSocket'} 114215 0.070 0.000 0.099 0.000 feedparser.py:308(__getitem__) 113341 0.046 0.000 0.173 0.000 feedparser.py:756(handle_data) 1 0.033 0.033 0.033 0.033 {method 'do_handshake' of '_ssl._SSLSocket'} 1 0.029 0.029 0.029 0.029 {method 'connect' of '_socket.socket'} 13 0.026 0.002 0.270 0.021 {method 'Parse' of 'pyexpat.xmlparser'} 113806 0.024 0.000 0.123 0.000 feedparser.py:373(get) 3455 0.023 0.000 0.024 0.000 {method 'sub' of 're.Pattern'} 113341 0.019 0.000 0.193 0.000 feedparser.py:2033(characters) 236 0.017 0.000 0.017 0.000 {method 'translate' of 'str'}
You can now see thatlatest_tutorial.py
actually spends most of its time working with sockets or handling data insidefeedparser
. The latter is one of the dependencies of the Real Python Reader that’s used to parse the tutorial feed.
You can usepstats
to get some idea of where your code is spending most of its time and then try to optimize anybottlenecks you find. You can also use the tool to understand the structure of your code better. For instance, the commandscallees
andcallers
will show you which functions call and are called by a given function.
You can also investigate certain functions. Check how much overheadTimer
causes by filtering the results with the phrasetimer
:
latest_tutorial.prof% stats timer 1393801 function calls (1389027 primitive calls) in 0.586 seconds Ordered by: internal time List reduced from 1443 to 8 due to restriction <'timer'> ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 0.000 0.000 timer.py:13(Timer) 1 0.000 0.000 0.000 0.000 timer.py:35(stop) 1 0.000 0.000 0.003 0.003 timer.py:3(<module>) 1 0.000 0.000 0.000 0.000 timer.py:28(start) 1 0.000 0.000 0.000 0.000 timer.py:9(TimerError) 1 0.000 0.000 0.000 0.000 timer.py:23(__post_init__) 1 0.000 0.000 0.000 0.000 timer.py:57(__exit__) 1 0.000 0.000 0.000 0.000 timer.py:52(__enter__)
Luckily,Timer
causes only minimal overhead. Usequit
to leave thepstats
browser when you’re done investigating.
For a more powerful interface into profile data, check outKCacheGrind. It uses its own data format, but you can convert data fromcProfile
usingpyprof2calltree
:
$pyprof2calltree-k-ilatest_tutorial.prof
This command will convertlatest_tutorial.prof
and open KCacheGrind to analyze the data.
The last option that you’ll try here for timing your code isline_profiler
.cProfile
can tell you which functions your code spends the most time in, but it won’t give you insights into which lines inside that function are the slowest. That’s whereline_profiler
can help you.
Note: You can also profile the memory consumption of your code. This falls outside the scope of this tutorial. However, you can check outmemory-profiler
if you need to monitor the memory consumption of your programs.
If you’d like to learn more about profiling, thenProfiling in Python: How to Find Performance Bottlenecks is here to help.
Note that line profiling takes time and adds a fair bit of overhead to your runtime. A normal workflow is first to usecProfile
to identify which functions to investigate and then runline_profiler
on those functions.line_profiler
isn’t part of the standard library, so you should first follow theinstallation instructions to set it up.
Before you run the profiler, you need to tell it which functions to profile. You do this by adding a@profile
decorator inside your source code. For example, to profileTimer.stop()
, you add the following insidetimer.py
:
@profiledefstop(self)->float:# The rest of the code is unchanged
Note that you don’t importprofile
anywhere. Instead, it’s automatically added to the global namespace when you run the profiler. You need to delete the line when you’re done profiling, though. Otherwise, you’ll get aNameError
.
Next, run the profiler usingkernprof
, which is part of theline_profiler
package:
$kernprof-llatest_tutorial.py
This command automatically saves the profiler data in a file calledlatest_tutorial.py.lprof
. You can see those results usingline_profiler
:
$python-mline_profilerlatest_tutorial.py.lprofTimer unit: 1e-06 sTotal time: 1.6e-05 sFile: /home/realpython/timer.pyFunction: stop at line 35#HitsTimePrHit%TimeLineContents=====================================35 @profile36 def stop(self) -> float:37 """Stop the timer, and report the elapsed time"""38 1 1.0 1.0 6.2 if self._start_time is None:39 raise TimerError(f"Timer is not running. ...")4041 # Calculate elapsed time42 1 2.0 2.0 12.5 elapsed_time = time.perf_counter() - self._start_time43 1 0.0 0.0 0.0 self._start_time = None4445 # Report elapsed time46 1 0.0 0.0 0.0 if self.logger:47 1 11.0 11.0 68.8 self.logger(self.text.format(elapsed_time))48 1 1.0 1.0 6.2 if self.name:49 1 1.0 1.0 6.2 self.timers[self.name] += elapsed_time5051 1 0.0 0.0 0.0 return elapsed_time
First, note that the time unit in this report is microseconds (1e-06 s
). Usually, the most accessible number to look at is%Time
, which tells you the percentage of the total time your code spends inside a function at each line. In this example, you can see that your code spends almost 70 percent of the time on line 47, which is the line that formats and prints the result of the timer.
In this tutorial, you’ve tried several different approaches to adding a Python timer to your code:
You used aclass to keep state and add a user-friendly interface. Classes are very flexible, and usingTimer
directly gives you full control over how and when to invoke the timer.
You used acontext manager to add features to a block of code and, if necessary, to clean up afterward. Context managers are straightforward to use, and addingwith Timer()
can help you more clearly distinguish your code visually.
You used adecorator to add behavior to a function. Decorators are concise and compelling, and using@Timer()
is a quick way to monitor your code’s runtime.
You’ve also learned why you should prefertime.perf_counter()
overtime.time()
when benchmarking code, as well as what other alternatives are useful when you’re optimizing your code.
Now you can add Python timer functions to your own code! Keeping track of how fast your program runs in your logs will help you monitor your scripts. Do you have ideas for other use cases where classes, context managers, and decorators play well together? Leave a comment down below!
For a deeper dive into Python timer functions, check out these resources:
codetiming
is the Python timer available on PyPI.time.perf_counter()
is a performance counter for precise timings.timeit
is a tool for comparing the runtimes of code snippets.cProfile
is a profiler for finding bottlenecks in scripts and programs.pstats
is a command-line tool for looking at profiler data.line_profiler
is a profiler for measuring individual lines of code.memory-profiler
is a profiler for monitoring memory usage.Now that you have some experience with Python timer functions, you can use the questions and answers below to check your understanding and recap what you’ve learned.
These FAQs are related to the most important concepts you’ve covered in this tutorial. Click theShow/Hide toggle beside each question to reveal the answer.
You measure execution time in Python using functions liketime.perf_counter()
, which provides high resolution timing suitable for performance testing.
The best way to time code in Python is to usetime.perf_counter()
due to its high precision, especially when timing short durations.
You usetime.perf_counter()
by calling it before and after the code block you want to measure, then calculate the elapsed time by finding the difference between the two calls.
You create a Python timer by encapsulating timing logic in classes, context managers, or decorators. This allows you to easily measure and log execution time across your code.
You track execution time by defining a timer class with.start()
and.stop()
methods, using it as a context manager with thewith
statement, or by decorating functions with a timer to automatically measure execution time.
🐍 Python Tricks 💌
Get a short & sweetPython Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.
AboutGeir Arne Hjelle
Geir Arne is an avid Pythonista and a member of the Real Python tutorial team.
» More about Geir ArneMasterReal-World Python Skills With Unlimited Access to Real Python
Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:
MasterReal-World Python Skills
With Unlimited Access to Real Python
Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:
What Do You Think?
What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.
Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students.Get tips for asking good questions andget answers to common questions in our support portal.
Keep Learning
Already have an account?Sign-In
Almost there! Complete this form and click the button below to gain instant access:
Python Decorators Q&A Transcript (PDF)