Movatterモバイル変換


[0]ホーム

URL:


— FREE Email Series —

🐍 Python Tricks 💌

Python Tricks Dictionary Merge

🔒 No spam. Unsubscribe any time.

Browse TopicsGuided Learning Paths
Basics Intermediate Advanced
apibest-practicescareercommunitydatabasesdata-sciencedata-structuresdata-vizdevopsdjangodockereditorsflaskfront-endgamedevguimachine-learningnumpyprojectspythontestingtoolsweb-devweb-scraping

Table of Contents

Python Timer Functions: Three Ways to Monitor Your Code

Python Timer Functions: Three Ways to Monitor Your Code

byGeir Arne Hjelle Dec 08, 2024intermediatepython

Table of Contents

Remove ads

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.
  • You can createcustom timer classes to encapsulate timing logic and reuse it across multiple parts of your program.
  • Usingdecorators lets you seamlessly add timing functionality to existing functions without altering their code.
  • You can leveragecontext managers to neatly measure execution time in specific code blocks, improving both resource management and code clarity.

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.

Python Timers

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.

Python Timer Functions

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.

Example: Download Tutorials

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:

Shell
$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:

Pythonlatest_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:

  • Line 1 importsfeed fromrealpython-reader. This module contains functionality for downloading tutorials from theReal Python feed.
  • Line 5 downloads the latest tutorial from Real Python. The number0 is an offset, where0 means the most recent tutorial,1 is the previous tutorial, and so on.
  • Line 7 prints the tutorial to the console.
  • Line 9 callsmain() when you run the script.

When you run this example, your output will typically look something like this:

Shell
$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.

Your First Python Timer

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:

Python
>>>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:

Pythonlatest_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:

Shell
$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.

A Python Timer Class

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:

Shell
$python-mpipinstallcodetiming

You can find more information aboutcodetiming later on in this tutorial, in the section namedThe Python Timer Code.

Understanding Classes in Python

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:

Python
>>>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:

Python
>>>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.

Creating a Python Timer Class

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:

Pythontimer.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 thatTimerErrorinherits 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:

Python
>>>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!

Using the Python Timer Class

Now applyTimer tolatest_tutorial.py. You only need to make a few changes to your previous code:

Pythonlatest_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:

Shell
$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.

Adding More Convenience and Flexibility

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:

  • Useadaptable text and formatting when reporting the time spent
  • Applyflexible logging, either to the screen, to a log file, or other parts of your program
  • Create a Python timer that canaccumulate over several invocations
  • Build aninformative representation of a 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:

Pythontimer.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:

Pythontimer.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:

Python
>>>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:

Pythontimer.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:

Python
>>>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:

Pythonlatest_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:

Shell
$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:

Python
classTimer:timers={}

Class variables can be accessed either directly on the class or through an instance of the class:

Python
>>>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:

  1. Looking up the elapsed time later in your code
  2. Accumulating timers with the same name

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:

Pythontimer.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:

Python
>>>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:

Pythonlatest_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:

Shell
$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:

Python
>>>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:

Python
 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:

Python
>>>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:

Pythontimer.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:

  • Readability: Your code will read more naturally if you carefully choose class and method names.
  • Consistency: Your code will be easier to use if you encapsulate properties and behaviors into attributes and methods.
  • Flexibility: Your code will be reusable if you use attributes with default values instead of hard-coded values.

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.

A Python Timer Context Manager

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:

  1. First, instantiate the class.
  2. Call.start() before the code block that you want to time.
  3. Call.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.

Understanding Context Managers in Python

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 thewithkeyword:

Python
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:

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:

Python
>>>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:

  1. Call.__enter__() when entering the context related to the context manager.
  2. Call.__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:

Python
# 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:

Python
>>>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:

Python
>>>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:

Python
>>>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.

Creating a Python Timer Context Manager

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:

Pythontimer.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:

Python
>>>fromtimerimportTimer>>>importtime>>>withTimer():...time.sleep(0.7)...Elapsed time: 0.7012 seconds

You should also note two more subtle details:

  1. .__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.

  2. .__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:

Python
>>>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.

Using the Python Timer Context Manager

Now you’ll learn how to use theTimer context manager to time the download of Real Python tutorials. Recall how you usedTimer earlier:

Pythonlatest_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:

Pythonlatest_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:

Shell
$pythonlatest_tutorial.pyElapsed time: 0.71 seconds#PythonTimerFunctions:ThreeWaystoMonitorYourCode[ ... ]

There are a few advantages to adding context manager capabilities to your Python timer class:

  • Low effort: You only need one extra line of code to time the execution of a block of code.
  • Readability: Invoking the context manager is readable, and you can more clearly visualize the code block that you’re timing.

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.

A Python Timer Decorator

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:

  1. UseTimer every time you call the function:

    Python
    withTimer("some_name"):do_something()

    If you calldo_something() in many places, then this will become cumbersome and hard to maintain.

  2. Wrap the code in your function inside a context manager:

    Python
    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.

Understanding Decorators in Python

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:

Python
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:

Python
>>>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:

Python
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():

Python
>>>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:

Python
>>>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:

Python
 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:

  • Line 1 starts the definition oftriple() and expects a function as an argument.
  • Lines 2 to 5 define the inner functionwrapper_triple().
  • Line 6 returnswrapper_triple().

This pattern is prevalent for defining decorators. The interesting parts are those happening inside the inner function:

  • Line 2 starts the definition ofwrapper_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.
  • Line 3 prints out the name of the decorated function and notes thattriple() has been applied to it.
  • Line 4 callsfunc(), the function thattriple() has decorated. It passes on all arguments passed towrapper_triple().
  • Line 5 triples the return value offunc() and returns it.

Try it out!knock() is a function that returns the wordPenny. See what happens if it’s tripled:

Python
>>>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:

Python
>>>@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:

Python
>>>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:

Python
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:

Python
>>>@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:

Python
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.

Creating a Python Timer Decorator

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:

Python
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:

Python
>>>@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:

Python
>>>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:

Python
>>>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:

Python
>>>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:

Pythontimer.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:

Python
>>>@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:

Python
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.

Using the Python Timer Decorator

Next up, you’ll redo thelatest_tutorial.py example one last time, using the Python timer as a decorator:

Pythonlatest_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:

Shell
$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:

  • Low effort: You only need one extra line of code to time the execution of a function.
  • Readability: When you add the decorator, you can note more clearly that your code will time the function.
  • Consistency: You only need to add the decorator when the function is defined. Your code will consistently time it every time it’s called.

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.

The Python Timer Code

You can expand the code block below to view the final source code for your Python timer:

Pythontimer.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:

Python
>>>fromtimerimportTimer

Timer is also available onPyPI, so an even easier option is to install it usingpip:

Shell
$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:

Python
>>>fromcodetimingimportTimer

Apart from the name andsome additional features,codetiming.Timer works exactly astimer.Timer. To summarize, you can useTimer in three different ways:

  1. As aclass:

    Python
    t=Timer(name="class")t.start()# Do somethingt.stop()
  2. As acontext manager:

    Python
    withTimer(name="context manager"):# Do something
  3. As adecorator:

    Python
    @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.

Other Python Timer Functions

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.

Using Alternative Python Timer Functions

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:

Python
>>>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:

Python
>>>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:

Python
>>>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():

Python
>>>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():

Python
>>>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:

Python
>>>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 updates
  • time.perf_counter(): benchmarking, most precise clock for short period
  • time.process_time(): profiling, CPU time of the process (Source)

As you can tell,perf_counter() is usually the best choice for your Python timer.

Estimating Running Time Withtimeit

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:

Python
>>>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:

Shell
$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:

Python
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.

Finding Bottlenecks in Your Code With Profilers

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:

Shell
$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:

Shell
$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:

Shell
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:

Shell
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:

Shell
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:

Shell
$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:

Python
@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:

Shell
$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:

Shell
$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.

Conclusion

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!

Resources

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.
  • KCachegrind is a GUI 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.

Frequently Asked Questions

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.

Python Tricks Dictionary Merge

AboutGeir Arne Hjelle

Geir Arne is an avid Pythonista and a member of the Real Python tutorial team.

» More about Geir Arne

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

MasterReal-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

MasterReal-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

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.


Looking for a real-time conversation? Visit theReal Python Community Chat or join the next“Office Hours” Live Q&A Session. Happy Pythoning!

Keep Learning

Related Topics:intermediatepython

Related Tutorials:

Keep reading Real Python by creating a free account or signing in:

Already have an account?Sign-In

Almost there! Complete this form and click the button below to gain instant access:

Python Decorators

Python Decorators Q&A Transcript (PDF)

🔒 No spam. We take your privacy seriously.


[8]ページ先頭

©2009-2025 Movatter.jp