[Rod Stephens Books]
Index Books Python Examples About Rod Contact
[Mastodon] [Bluesky]
[Build Your Own Ray Tracer With Python]

[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

Title: Improve Pygame performance in Python

[Performance results for various Pygame tests in Python]

While working on the post Display Pygame's named colors in Python, I got to thinking about the call to pygame.display.update. That method makes Pygame update the screen to show whatever you've been drawing. That begs the question, "Do you need to call update if nothing in the drawing has changed?"

Answers online are a bit confusing. It turns out that (as near as I can tell) you do not need to call update if nothing has changed. In fact, the update method can take option parameters indicating the parts of the window that need to be updated. If nothing has been updated, you could pass it a tiny rectangle so it wouldn't waste time updating everything.

This program performs four tests to see how much CPU time we can save for a program where the drawing doesn't change.

Getting Started

The program displays swatches of Pygame's named colors. It works mostly the same way the previous post Display Pygame's named colors in Python does. See that post for details about how the program displays the swatches.

To make the display fit on the screen a little better, this program doesn't display any of the "gray" or "grey" colors (or colours). Here's the code it uses to skip those.

# Remove grays and greys. colors = {key:value for key,value in pygame.color.THECOLORS.items() if not ('gray' in key or 'grey' in key )}

This code uses a dictionary comprehension to select the non-gray/grey color names and their values from the pygame.color.THECOLORS dictionary.

It then uses the following code to get ready for the tests.

# Initialize Pygame. pygame.init() # Set up the window. width = 1200 height = 700 surface = pygame.display.set_mode((width, height)) pygame.display.set_caption('pygame_updates') # Make the swatches. swatches = make_swatches(width, height) test_seconds = 10

This code initializes Pygame, creates the window, and calls make_swatches to prepare the objects that represent the color swatches. (See the earlier post for details.) It then sets test_seconds to make each trial last for 10 seconds. You can adjust that if you like.

Test 1

The first test runs the way most Pygame programs run. It enters an event loop where it checks for events (like the user clicking the window's close button) and draws whatever the program needs to display. Here's this test's code.

stop_time = time.time() + test_seconds start_process_time = time.process_time() running = True while running: if time.time() > stop_time: running = False for event in pygame.event.get(): # If the user clicks the close button, quit. if event.type == pygame.QUIT: running = False # Set background color. surface.fill((255, 255, 255)) # Draw the color swatches. for swatch in swatches: swatch.draw(surface) # Update the display. pygame.display.update() elapsed = time.process_time() - start_process_time print(f'Draw always: {elapsed:.4f} seconds')

The code first saves the current clock time and the process's CPU time. It then runs its event loop. When test_seconds have passed, it ends the loop and displays the amount of elapsed CPU time.

Test 2

The second test only draws the color chart once. It still uses an event loop, but the loop only checks for events, it doesn't keep drawing the chart.

Here's its code.

stop_time = time.time() + test_seconds start_process_time = time.process_time() # Draw the chart. # Set background color. surface.fill((255, 255, 255)) # Draw the color swatches. for swatch in swatches: swatch.draw(surface) # Update the display. pygame.display.update() running = True while running: if time.time() > stop_time: running = False for event in pygame.event.get(): # If the user clicks the close button, quit. if event.type == pygame.QUIT: running = False elapsed = time.process_time() - start_process_time print(f'Draw once: {elapsed:.4f} seconds')

This is pretty straightforward.

Test 3

The third test adds a new technique. Before the event loop, it creates a pygame.time.Clock. The inside the loop, it calls the clock's tick method, passing it the value max_fps. That method then pauses if necessary to ensure that the program doesn't execute more than the given number of frames per second.

In the test I set max_fps to 10. For an animated program you should probably set this to at least 20 and probably 30 to give smooth animation. In this example, however, the graphic don't change, so we can get away with a slower frame rate. What this means is that the program won't detect events (like the user closing the window) as often, but I think if it reacts within 1/10 of a second, you probably won't mind.

Here's its code.

stop_time = time.time() + test_seconds start_process_time = time.process_time() clock = pygame.time.Clock() max_fps = 10 running = True while running: if time.time() > stop_time: running = False for event in pygame.event.get(): # If the user clicks the close button, quit. if event.type == pygame.QUIT: running = False # Set background color. surface.fill((255, 255, 255)) # Draw the color swatches. for swatch in swatches: swatch.draw(surface) # Update the display. pygame.display.update() elapsed_ticks = clock.tick(max_fps) elapsed = time.process_time() - start_process_time print(f'Draw always, limited fps: {elapsed:.4f} seconds')

Test 4

The final test combines both of the previous techniques: not redrawing the graphics inside the event loop and using clock.tick to limit the frame rate. Here's its code.

stop_time = time.time() + test_seconds start_process_time = time.process_time() # Draw the chart. clock = pygame.time.Clock() max_fps = 10 # Set background color. surface.fill((255, 255, 255)) # Draw the color swatches. for swatch in swatches: swatch.draw(surface) # Update the display. pygame.display.update() running = True while running: if time.time() > stop_time: running = False for event in pygame.event.get(): # If the user clicks the close button, quit. if event.type == pygame.QUIT: running = False elapsed_ticks = clock.tick(max_fps) elapsed = time.process_time() - start_process_time print(f'Draw once, limited fps: {elapsed:.4f} seconds')

Now that you know how the other tests work, this one should be easy to understand.

Conclusion

Here's the program's output.

Draw always: 9.9375 seconds Draw once: 9.9844 seconds Draw always, limited fps: 0.9375 seconds Draw once, limited fps: 0.0156 seconds

You can see that the first two tests use almost as much CPU time as real time. The third test, which limits the program's frame rate, uses only about 1/10th as much time.

The fourth test, which limits the frame rate and doesn't redraw the graphics inside the event loop uses only about 1/64th as much CPU time as real time!

If you don't mind hammering away on your CPU, then you can do things as in Test 1. However, that uses more electricity (which you have to pay for), and heats up your CPU. You can save energy if you limit the frame rate and only redraw parts of the graphics that change.

Download the example to experiment with it and to see additional details.

© 2025 Rocky Mountain Computer Consulting, Inc. All rights reserved.