Skip to content

Optimization Tips

PythonExtra is fast enough for real-time games — if you’re careful.
But if your app starts lagging, there are three usual culprits:

  1. Drawing too much
  2. Inefficient algorithms
  3. Memory allocation & GC abuse

This page shows how to spot those issues and fix them, with concrete examples.


1. Avoid Unnecessary Overdraw

You can easily get 20+ FPS with full-screen redraws — as long as you’re not redrawing what doesn’t need to change.

❌ Bad: Redrawing things you’re about to overwrite

# Clear the screen, then draw a full tileset right over it
drect(0, 0, DWIDTH-1, DHEIGHT-1, C_WHITE)

for y in range(0, DHEIGHT, 16):
    for x in range(0, DWIDTH, 16):
        drect(x, y, x+15, y+15, C_GRAY)  # Overwrites previous draw

You’re wasting time filling pixels with white only to overwrite them immediately.

âś… Better: Just draw the tiles

for y in range(0, DHEIGHT, 16):
    for x in range(0, DWIDTH, 16):
        drect(x, y, x+15, y+15, C_GRAY)

Unless you need to clear parts of the screen (like UI or old objects), don’t redraw them.


2. Avoid Rebuilding Data Structures

Constantly creating new lists/tuples/objects every frame leads to spikes in garbage collection, especially if they’re small.

❌ Bad: Reallocating every frame

# Builds a new list of coords every time
pixels = [(x * 10, y * 10) for x, y in snake]

This creates N tuples per frame, and GC hates it.

âś… Better: Use preallocated buffers (but still not perfect)

# Still makes tuples, but reuses the list
pixels = [(0, 0)] * 100

for i, (x, y) in enumerate(snake):
    pixels[i] = (x * 10, y * 10)

âś…âś… Even better: Use flat bytes or bytearray

# 100 pairs = 200 bytes
buf = bytearray(200)

for i, (x, y) in enumerate(snake):
    buf[i*2] = x
    buf[i*2+1] = y
  • bytes/bytearray are 4x cheaper per value than tuples
  • Especially useful when values fit in 0–255 range (like grid coordinates or sprite IDs)
  • You get CPU cache friendliness and less GC overhead

3. Avoid Allocating Thousands of Small Values

If you’re building a huge array of tiny tuples, you’re feeding the garbage collector a buffet of objects it has to track and clean.

❌ Bad: 1,000 tiny tuples

positions = [(x, x) for x in range(1000)]

That’s 1,000 allocations — and it gets worse if you do this in a loop.

âś… Better: Use a flat array or shared buffer

positions = bytearray(2000)

for i in range(1000):
    positions[i*2] = i % 256
    positions[i*2+1] = i // 4
  • Uses one object instead of 1,000
  • Fits better in memory
  • No GC required

This trick is essential for tilemaps, image buffers, path grids, etc.


4. Avoid Doing Heavy Work Every Frame

❌ Bad: Sorting a list every tick

def find_closest(entities):
    return sorted(entities, key=distance)[0]

# In the main loop
closest = find_closest(enemies)

Even for 30–50 items, this adds up fast.

âś… Better: Cache or stagger expensive work

if frame % 10 == 0:
    closest = min(enemies, key=distance)

Spread expensive calculations across frames, or only update when data changes.


TL;DR – What to Watch

ProblemAvoid thisDo this instead
OverdrawingClearing then overdrawingOnly draw what’s visible/changed
GC abuseAllocating lists/tuples constantlyPreallocate or use bytearray
Algorithm bottlenecksSorting/pathfinding every frameUpdate on timer or when needed only

Extra Notes

  • drect() includes both x2/y2 — so full-screen is drect(0, 0, DWIDTH-1, DHEIGHT-1, color)
  • dupdate() is mandatory to display changes. Call it once per frame.
  • For input-heavy loops, clearevents() and cleareventflips() are your friends.
  • Partial redraws (only updating specific screen regions) can improve performance but are tricky. Unless you’re drawing fewer than 3 things per frame, full redraw is simpler and usually fast enough.

Need help diagnosing your frame drops?
Ask the 👨‍💻 PythonExtra ChatGPT — just paste your loop or logic!