Skip to content

raylib (Python) cheatsheet

This cheatsheet is written for the PyPI package named raylib (a.k.a. raylib-python-cffi), whose public, “pythonic” API lives in the pyray module and targets raylib 5.5. (Electron Studio) raylib itself is currently at v5.5 (latest release on the official raylib repo). (GitHub)


0) Quick start: install + one-file template

Install

uv add raylib

(That installs the binding; you'll write code using pyray.) (PyPI)

Idiomatic import style

Prefer a module alias (clean namespace, easy grepping):

import pyray as rl

The docs/quickstart often use from pyray import * for brevity, but it’s not ideal for larger projects. (PyPI)


1) The canonical raylib loop (Python)

Minimal, “always correct” skeleton

import pyray as rl

def main() -> None:
    rl.init_window(960, 540, "raylib-python-cffi template")
    rl.set_target_fps(60)

    try:
        while not rl.window_should_close():
            rl.begin_drawing()
            rl.clear_background(rl.RAYWHITE)

            rl.draw_text("Hello raylib!", 20, 20, 20, rl.DARKGRAY)

            rl.end_drawing()
    finally:
        rl.close_window()

if __name__ == "__main__":
    main()

Core lifecycle calls shown above are the ones you always want in the right order:

Idiomatic Python tip: wrap the loop with try/finally so you always close the window, even if you hit an exception.


2) “Idiomatic Python” structure for raylib games

raylib is fundamentally a C-style “do stuff every frame” library. In Python, the most maintainable pattern is:

  • Keep state in a dataclass
  • Split update and draw
  • Use explicit dependencies (pass state in/out) instead of globals

A clean structure you can scale

from dataclasses import dataclass
import pyray as rl

@dataclass
class Game:
    pos: rl.Vector2
    vel: rl.Vector2
    radius: float = 24.0

def update(g: Game, dt: float) -> None:
    speed = 240.0
    if rl.is_key_down(rl.KeyboardKey.KEY_A): g.pos.x -= speed * dt
    if rl.is_key_down(rl.KeyboardKey.KEY_D): g.pos.x += speed * dt
    if rl.is_key_down(rl.KeyboardKey.KEY_W): g.pos.y -= speed * dt
    if rl.is_key_down(rl.KeyboardKey.KEY_S): g.pos.y += speed * dt

def draw(g: Game) -> None:
    rl.clear_background(rl.RAYWHITE)
    rl.draw_circle(int(g.pos.x), int(g.pos.y), g.radius, rl.BLUE)
    rl.draw_text("WASD to move", 20, 20, 20, rl.DARKGRAY)

def main() -> None:
    rl.init_window(960, 540, "Idiomatic raylib Python")
    rl.set_target_fps(60)

    g = Game(pos=rl.Vector2(480, 270), vel=rl.Vector2(0, 0))
    try:
        while not rl.window_should_close():
            dt = rl.get_frame_time()

            update(g, dt)

            rl.begin_drawing()
            draw(g)
            rl.end_drawing()
    finally:
        rl.close_window()

if __name__ == "__main__":
    main()

Relevant API pieces used here:


3) Data types & “Pythonic” arguments (tuples vs structs)

A big convenience of this binding is that many functions accept either:

  • a proper struct class (Vector2, Rectangle, Camera2D, …)
  • or a plain (x, y) tuple / list

Examples (types from the docs):

Rule of thumb

  • Prototyping / tiny scripts: tuples are fine.
  • Real games / hot loops: prefer struct instances to avoid repeated conversions and to keep your code self-documenting.

4) Window configuration & app behavior

Config flags (vsync, resizable window, etc.)

Use set_config_flags(flags: int) to set init-time window flags. (Electron Studio) Flags live in ConfigFlags (e.g. FLAG_VSYNC_HINT, FLAG_WINDOW_RESIZABLE, …). (Electron Studio)

Example:

import pyray as rl

rl.set_config_flags(
    rl.ConfigFlags.FLAG_VSYNC_HINT | rl.ConfigFlags.FLAG_WINDOW_RESIZABLE
)
rl.init_window(960, 540, "Flags example")

(Electron Studio)

Change exit key (default ESC)

rl.set_exit_key(rl.KeyboardKey.KEY_Q)  # quit with Q

(Electron Studio)

Toggle fullscreen

if rl.is_key_pressed(rl.KeyboardKey.KEY_F11):
    rl.toggle_fullscreen()

(Electron Studio)


5) Input cheatsheet (keyboard + mouse)

Keyboard

Mouse

Example:

mp = rl.get_mouse_position()
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
    print("Clicked at", mp.x, mp.y)

(Electron Studio)


6) Drawing cheatsheet (2D essentials)

Always draw inside begin_drawing() / end_drawing()

Clear the frame

rl.clear_background(rl.RAYWHITE)

(Electron Studio)

Common primitives

Colors

This binding exposes common named colors like RAYWHITE, DARKGRAY, etc. (Electron Studio)


7) Textures & sprites

Load / draw / unload a texture

tex = rl.load_texture("assets/player.png")
# ...
rl.draw_texture(tex, 100, 100, rl.WHITE)
# ...
rl.unload_texture(tex)

API:

Transform a sprite (position/rotation/scale)

Use draw_texture_ex(...). (Electron Studio)

rl.draw_texture_ex(tex, (200, 200), rotation=45.0, scale=2.0, tint=rl.WHITE)

(Electron Studio)

Sprite sheets (source/dest rectangles)

Use draw_texture_pro(...) (source rect, dest rect, origin, rotation, tint). (Electron Studio)

src = rl.Rectangle(0, 0, 32, 32)
dst = rl.Rectangle(200, 200, 64, 64)     # scale x2
origin = (32, 32)                        # rotate about center
rl.draw_texture_pro(tex, src, dst, origin, rotation=0.0, tint=rl.WHITE)

(Electron Studio)


8) Cameras (2D & 3D)

2D Camera

Create a Camera2D and draw your world inside begin_mode_2d(camera) / end_mode_2d(). (Electron Studio)

cam = rl.Camera2D(
    offset=(480, 270),   # screen center
    target=(0, 0),       # world point at center
    rotation=0.0,
    zoom=1.0
)

rl.begin_drawing()
rl.clear_background(rl.RAYWHITE)

rl.begin_mode_2d(cam)
# draw WORLD here (positions in world space)
rl.draw_circle(0, 0, 10, rl.RED)
rl.end_mode_2d()

# draw UI here (screen space)
rl.draw_text("2D camera", 20, 20, 20, rl.DARKGRAY)
rl.end_drawing()

(Electron Studio)

3D Camera

Create Camera3D and draw inside begin_mode_3d(camera) / end_mode_3d(). (Electron Studio)

Minimal 3D scene helpers:

cam3 = rl.Camera3D(
    position=(4, 4, 4),
    target=(0, 0, 0),
    up=(0, 1, 0),
    fovy=45.0,
    projection=rl.CameraProjection.CAMERA_PERSPECTIVE
)

rl.begin_drawing()
rl.clear_background(rl.RAYWHITE)

rl.begin_mode_3d(cam3)
rl.draw_grid(20, 1.0)
rl.draw_cube((0, 0.5, 0), 1.0, 1.0, 1.0, rl.BLUE)
rl.end_mode_3d()

rl.end_drawing()

(Electron Studio)


9) Render textures (draw to a texture, then draw that texture)

Core calls

Example:

rt = rl.load_render_texture(320, 180)

# draw scene into rt
rl.begin_texture_mode(rt)
rl.clear_background(rl.BLANK)
rl.draw_circle(160, 90, 40, rl.RED)
rl.end_texture_mode()

# draw rt.texture onto screen
rl.begin_drawing()
rl.clear_background(rl.RAYWHITE)
rl.draw_texture_ex(rt.texture, (0, 0), 0.0, 3.0, rl.WHITE)  # scale up 3x
rl.end_drawing()

(Electron Studio)


10) Audio (sounds + streaming music)

One-time init/teardown

Sound effects

Music streams (must be updated every frame)

Example:

rl.init_audio_device()
snd = rl.load_sound("assets/jump.wav")
msc = rl.load_music_stream("assets/theme.ogg")
rl.play_music_stream(msc)

try:
    while not rl.window_should_close():
        rl.update_music_stream(msc)

        if rl.is_key_pressed(rl.KeyboardKey.KEY_SPACE):
            rl.play_sound(snd)

        rl.begin_drawing()
        rl.clear_background(rl.RAYWHITE)
        rl.draw_text("SPACE: play sound", 20, 20, 20, rl.DARKGRAY)
        rl.end_drawing()
finally:
    rl.unload_sound(snd)
    rl.unload_music_stream(msc)
    rl.close_audio_device()

(Electron Studio)


11) Collision helpers (2D)

Two common ones:

  • Rectangle-vs-rectangle: check_collision_recs(rec1, rec2) -> bool (Electron Studio)
  • Point-in-triangle: check_collision_point_triangle(...) -> bool (Electron Studio)

Pattern:

r1 = rl.Rectangle(10, 10, 50, 50)
r2 = rl.Rectangle(40, 40, 50, 50)

if rl.check_collision_recs(r1, r2):
    rl.draw_text("hit!", 20, 80, 20, rl.RED)

(Electron Studio)


12) Must-remember “paired calls” (don’t leak resources)

Frames

Camera modes

Render textures

GPU textures

Window

Audio


13) Performance & “idiomatic Python” pitfalls (important!)

1) Avoid doing “a thousand tiny C calls” per frame

Crossing the Python↔C boundary costs time. The project’s own performance notes call this out and recommend doing most calculations in Python and only calling into raylib when you actually need to draw/play audio/etc. (PyPI)

Practical advice:

  • Batch your drawing where possible.
  • Avoid per-frame allocations of lots of new Vector2/Rectangle objects in tight loops.
  • Keep your update logic in Python data (floats, lists), then convert to raylib structs when drawing.

2) Prefer explicit imports

For real projects:

import pyray as rl

Star imports are fine for learning, but they make refactors harder (and can shadow names). (The official quickstart uses star import for simplicity.) (PyPI)

3) Centralize resource ownership

Have one place that loads/unloads textures, sounds, fonts. Make it impossible to “forget” an unload.


14) “Latest API” notes (what to rely on)

  • raylib upstream: currently v5.5. (GitHub)
  • Python binding docs used here: explicitly “Python Bindings for Raylib 5.5”. (Electron Studio)
  • PyPI package: raylib (raylib-python-cffi), with a 5.5.x release line (e.g. 5.5.0.4 shown in the PyPI release history). (PyPI)

If you stick to the function names/signatures shown above (all pulled from the binding’s current docs), you’re aligned with the latest documented Python API for raylib 5.5. (Electron Studio)


If you want, tell me whether you’re building 2D-only, 3D, or a tools/UI app, and I’ll condense this into a one-page “printable” cheatsheet tailored to that use case (with just the functions you’ll touch daily).