Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

EktuPy Documentation

EktuPy is a Python library for creating Scratch-like games in the browser. It runs entirely in the browser using PyScript/Pyodide, making it perfect for educational programming.

EktuPy Interface

Features

  • Scratch-like API: Familiar blocks-style programming with Python syntax
  • Sprite-based graphics: Create and control multiple sprites
  • Event-driven programming: Respond to keyboard, mouse, and custom events
  • Per-sprite scripts: Each sprite can have its own independent code (like Scratch)
  • Non-blocking animations: Smooth glides and waits without freezing
  • Pen drawing: Draw lines and shapes on the stage
  • Collision detection: Detect when sprites touch each other or edges
  • Cloning: Create copies of sprites at runtime

Quick Start

from ektupy import Stage, Sprite, on_start, on_forever, key_pressed

# Create stage and sprite
stage = Stage()
cat = Sprite("cat")
stage.add_sprite(cat)

@on_start
def setup():
    cat.go_to(0, 0)
    cat.say("Hello! Use arrow keys to move me!")

@on_forever
def game_loop():
    if key_pressed("right arrow"):
        cat.change_x(5)
    if key_pressed("left arrow"):
        cat.change_x(-5)

Documentation

Coordinate System

EktuPy uses a Scratch-like coordinate system:

  • Stage size is fixed at 960x720 pixels
  • Center of stage is (0, 0)
  • X increases to the right (-480 to 480)
  • Y increases upward (-360 to 360)
  • Direction 0 = up, 90 = right, 180 = down, -90 = left

Getting Started with EktuPy

This guide will help you create your first EktuPy program.

The Interface

When you open EktuPy, you’ll see:

EktuPy Interface

  1. Code Editor (left): Write your Python code here
  2. Stage (right): Your sprites appear and move here
  3. Console (bottom right): See output from print() and error messages
  4. Toolbar: Run, Stop, Reset buttons and example loader

Your First Program

Let’s create a simple program that shows a sprite with a speech bubble.

from ektupy import Stage, Sprite, on_start

# Create the stage (fixed at 960x720 pixels)
stage = Stage()

# Create a sprite using its asset name
snake = Sprite("snake")

# Add the sprite to the stage
stage.add_sprite(snake)

# This runs when the program starts
@on_start
def setup():
    snake.go_to(0, 0)
    snake.say("Hello, World!")

Click Run (or press Ctrl+Enter) to see the result:

Hello World

Understanding the Code

Stage

The Stage is the canvas where everything happens. It has a fixed size of 960x720 pixels:

stage = Stage()  # Creates a 960x720 stage
stage.set_background("#87CEEB")  # Set background color

Sprites

Sprite objects are the characters in your program. Use asset names to create them:

sprite = Sprite("snake")  # Use asset name
stage.add_sprite(sprite)

Event Decorators

Decorators tell EktuPy when to run your functions:

@on_start
def setup():
    # Runs once when program starts
    pass

@on_forever
def game_loop():
    # Runs continuously (every frame)
    pass

Adding Movement

Let’s make the sprite move with arrow keys:

from ektupy import Stage, Sprite, on_start, on_forever, key_pressed

stage = Stage()
snake = Sprite("snake")
stage.add_sprite(snake)

@on_start
def setup():
    snake.go_to(0, 0)
    snake.say("Use arrow keys to move me!", 3)

@on_forever
def game_loop():
    if key_pressed("right arrow"):
        snake.direction = 90
        snake.move(5)
    if key_pressed("left arrow"):
        snake.direction = -90
        snake.move(5)
    if key_pressed("up arrow"):
        snake.direction = 0
        snake.move(5)
    if key_pressed("down arrow"):
        snake.direction = 180
        snake.move(5)

Movement

Using Print for Debugging

You can use print() to output messages to the console:

@on_start
def setup():
    print("Program started!")
    snake.go_to(0, 0)
    print(f"Snake position: ({snake.x}, {snake.y})")

Output appears in the Console panel. Errors appear in red.

Non-Blocking Waits

Use wait() to pause without freezing the program:

from ektupy import Stage, Sprite, on_start, wait

stage = Stage()
cat = Sprite("cat")
stage.add_sprite(cat)

@on_start
def setup():
    cat.say("Hello!")
    wait(2)  # Wait 2 seconds
    cat.say("Goodbye!")
    wait(2)
    cat.say("")  # Clear speech bubble

Per-Sprite Scripts

Each sprite can have its own independent code:

from ektupy import Stage, Sprite

stage = Stage()

cat = Sprite("cat")
dog = Sprite("dog")

stage.add_sprite(cat)
stage.add_sprite(dog)

# Cat's scripts
@cat.on_start
def cat_setup():
    cat.go_to(-100, 0)
    cat.say("I'm the cat!")

@cat.on_key_press("up arrow")
def cat_up():
    cat.change_y(10)

# Dog's scripts
@dog.on_start
def dog_setup():
    dog.go_to(100, 0)
    dog.say("I'm the dog!")

@dog.on_key_press("w")
def dog_up():
    dog.change_y(10)

Two Sprites

Next Steps

  • Read the API Reference for all available methods
  • Check out Examples for more code samples
  • Configure the editor in Settings (theme, LSP, type checking)
  • Experiment with drawing using the pen methods
  • Try creating clones for particle effects or enemies

EktuPy Examples

This page contains example programs demonstrating EktuPy features. Each example shows a different aspect of the API.


Hello World

The simplest EktuPy program - display a sprite with a speech bubble.

from ektupy import Stage, Sprite, on_start

stage = Stage()
snake = Sprite("snake")
stage.add_sprite(snake)

@on_start
def setup():
    snake.go_to(0, 0)
    snake.say("Hello! Use arrow keys to move me!", 3)

Key concepts: Stage(), Sprite(), @on_start, say()

Hello World


Arrow Key Movement

Move a sprite around the stage using arrow keys.

from ektupy import Stage, Sprite, on_start, on_forever, key_pressed

stage = Stage()
snake = Sprite("snake")
stage.add_sprite(snake)

@on_start
def setup():
    snake.go_to(0, 0)
    snake.say("Hello! Use arrow keys to move me!", 3)

@on_forever
def game_loop():
    if key_pressed("right arrow"):
        snake.direction = 90
        snake.move(5)
    if key_pressed("left arrow"):
        snake.direction = -90
        snake.move(5)
    if key_pressed("up arrow"):
        snake.direction = 0
        snake.move(5)
    if key_pressed("down arrow"):
        snake.direction = 180
        snake.move(5)

Key concepts: @on_forever, key_pressed(), direction, move()

Movement


Bouncing Ball

A sprite that bounces around the stage continuously.

from ektupy import Stage, Sprite, on_start, on_forever

stage = Stage()
snake = Sprite("snake")
stage.add_sprite(snake)

@on_start
def setup():
    snake.go_to(0, 0)
    snake.direction = 45
    snake.set_rotation_style("all around")

@on_forever
def game_loop():
    snake.move(5)
    snake.bounce_if_on_edge()

Key concepts: set_rotation_style(), bounce_if_on_edge()

Bounce


Drawing with Pen

Use the pen to draw shapes as the sprite moves.

from ektupy import Stage, Sprite, on_start, on_forever, key_pressed

stage = Stage()
stage.set_background("#222222")

pen = Sprite("snake")
stage.add_sprite(pen)

@on_start
def setup():
    pen.go_to(0, 0)
    pen.set_pen_color("#00FF00")
    pen.set_pen_size(3)
    pen.pen_down()

@on_forever
def draw():
    if key_pressed("right arrow"):
        pen.direction = 90
        pen.move(3)
    if key_pressed("left arrow"):
        pen.direction = -90
        pen.move(3)
    if key_pressed("up arrow"):
        pen.direction = 0
        pen.move(3)
    if key_pressed("down arrow"):
        pen.direction = 180
        pen.move(3)
    if key_pressed("c"):
        pen.clear()
    if key_pressed("space"):
        pen.pen_up()
        pen.go_to(0, 0)
        pen.pen_down()

Key concepts: pen_down(), pen_up(), set_pen_color(), set_pen_size(), clear()

Drawing


Sound Effects

Play sounds with volume and pitch control.

from ektupy import Stage, Sprite, on_start, on_key_press
from ektupy import load_sound, start_sound, stop_all_sounds
from ektupy import set_volume, change_volume, get_volume
from ektupy import set_pitch_effect, clear_sound_effects

stage = Stage()
stage.set_background("#2d3436")

snake = Sprite("snake")
stage.add_sprite(snake)

@on_start
def setup():
    snake.go_to(0, 0)
    snake.size = 100

    # Load sounds using asset names
    load_sound("sfx_coin_single1")
    load_sound("sfx_movement_jump1")
    load_sound("sfx_menu_select1")

    # Set initial volume
    set_volume(70)

    snake.say("Press: C=coin J=jump\nUP/DOWN=volume")

@on_key_press("c")
def play_coin():
    start_sound("sfx_coin_single1")
    snake.say(f"Coin! Vol: {get_volume()}%")

@on_key_press("j")
def play_jump():
    start_sound("sfx_movement_jump1")
    snake.say(f"Jump! Vol: {get_volume()}%")

@on_key_press("up arrow")
def volume_up():
    change_volume(10)
    snake.say(f"Volume: {get_volume()}%")

@on_key_press("down arrow")
def volume_down():
    change_volume(-10)
    snake.say(f"Volume: {get_volume()}%")

@on_key_press("p")
def pitch_up():
    set_pitch_effect(60)
    snake.say("High pitch!")

@on_key_press("n")
def pitch_normal():
    clear_sound_effects()
    snake.say("Normal pitch")

@on_key_press("s")
def stop_sounds():
    stop_all_sounds()
    snake.say("Stopped!")

Key concepts: load_sound(), start_sound(), set_volume(), change_volume(), get_volume(), set_pitch_effect(), clear_sound_effects(), stop_all_sounds()

Sound Effects


Costume Animation

Switch between costumes to create animation effects.

from ektupy import Stage, Sprite, on_start, on_forever

stage = Stage()
stage.set_background("#2d3436")

# Create a snake sprite - automatically loads all costumes
snake = Sprite("snake")
stage.add_sprite(snake)

frame_count = 0

@on_start
def setup():
    snake.go_to(0, 0)
    snake.size = 150
    # Show available costumes
    print("Available costumes:", snake.costumes)
    snake.say("I can change colors!", 2)

@on_forever
def animate():
    global frame_count
    frame_count += 1

    # Move in a circle
    snake.turn_right(3)
    snake.move(5)

    # Change costume every 20 frames
    if frame_count % 20 == 0:
        snake.next_costume()
        print(f"Now wearing: {snake.costume_name}")

Key concepts: costumes, next_costume(), costume_name, switch_costume()

Costume Animation


Ghost Effect (Transparency)

Make sprites fade in and out using the ghost effect.

from ektupy import Stage, Sprite, on_start, on_forever, on_key_press

stage = Stage()
stage.set_background("#1a1a2e")

ghost = Sprite("snake")
stage.add_sprite(ghost)

fade_direction = 1  # 1 = fading out, -1 = fading in
ghost_value = 0

@on_start
def setup():
    ghost.go_to(0, 0)
    ghost.size = 150
    ghost.say("I fade in and out!\nPress SPACE to reset", 3)

@on_forever
def animate():
    global fade_direction, ghost_value

    # Change ghost effect (transparency)
    ghost_value += fade_direction * 2

    # Reverse direction at limits
    if ghost_value >= 80:
        fade_direction = -1
    elif ghost_value <= 0:
        fade_direction = 1

    ghost.set_ghost_effect(ghost_value)

    # Move in a circle
    ghost.turn_right(2)
    ghost.move(3)
    ghost.bounce_if_on_edge()

@on_key_press("space")
def reset():
    ghost.set_ghost_effect(0)
    ghost.clear_graphic_effects()
    ghost.go_to(0, 0)
    ghost.say("Reset!")

Key concepts: set_ghost_effect(), change_ghost_effect(), clear_graphic_effects()

Ghost Effect


Sprite Layers

Control which sprites appear in front of others.

from ektupy import Stage, Sprite, on_start, on_key_press, wait

stage = Stage()
stage.set_background("#87CEEB")

# Create three overlapping sprites
red = Sprite("snake")
green = Sprite("snake")
blue = Sprite("snake")

stage.add_sprite(red)
stage.add_sprite(green)
stage.add_sprite(blue)

@on_start
def setup():
    # Position sprites overlapping
    red.go_to(-50, 0)
    red.size = 150

    green.go_to(0, 0)
    green.size = 150

    blue.go_to(50, 0)
    blue.size = 150

    red.say("R=front")
    green.say("G=front")
    blue.say("B=front")

@on_key_press("r")
def red_front():
    red.go_to_front_layer()
    red.say("I'm in front!")
    green.say("")
    blue.say("")

@on_key_press("g")
def green_front():
    green.go_to_front_layer()
    green.say("I'm in front!")
    red.say("")
    blue.say("")

@on_key_press("b")
def blue_front():
    blue.go_to_front_layer()
    blue.say("I'm in front!")
    red.say("")
    green.say("")

@on_key_press("space")
def reset_order():
    red.go_to_back_layer()
    green.go_forward_layers(1)
    blue.go_to_front_layer()
    red.say("Back")
    green.say("Middle")
    blue.say("Front")

Key concepts: go_to_front_layer(), go_to_back_layer(), go_forward_layers(), go_backward_layers()

Sprite Layers


Stamping Patterns

Use stamp() to leave copies of the sprite on the stage.

from ektupy import Stage, Sprite, on_start, on_key_press

stage = Stage()
stage.set_background("#1a1a2e")

snake = Sprite("snake")
stage.add_sprite(snake)

@on_start
def setup():
    snake.go_to(0, 0)
    snake.size = 30
    snake.say("Press S=spiral G=grid C=clear", 3)

@on_key_press("s")
def draw_spiral():
    snake.go_to(0, 0)
    snake.direction = 0
    snake.size = 20

    # Draw a spiral pattern using stamps
    for i in range(36):
        snake.stamp()
        snake.move(10 + i * 2)
        snake.turn_right(30)
        snake.size = 20 + i

    snake.say("Spiral complete!")

@on_key_press("c")
def clear_stamps():
    snake.clear()
    snake.go_to(0, 0)
    snake.size = 30
    snake.say("Cleared!")

@on_key_press("g")
def draw_grid():
    snake.size = 20

    # Draw a grid pattern
    for row in range(-4, 5):
        for col in range(-6, 7):
            snake.go_to(col * 60, row * 60)
            snake.stamp()

    snake.go_to(0, 0)
    snake.say("Grid complete!")

Key concepts: stamp(), clear()

Stamping


Mouse Tracking

Follow the mouse and detect proximity.

from ektupy import Stage, Sprite, on_start, on_forever
from ektupy import mouse_x, mouse_y

stage = Stage()
stage.set_background("#2d5a27")

snake = Sprite("snake")
stage.add_sprite(snake)

@snake.on_start
def setup():
    snake.go_to(0, 0)
    snake.size = 80

@snake.on_forever
def track_mouse():
    # Calculate distance to mouse
    dist = snake.distance_to_mouse()

    # Check if touching mouse pointer
    if snake.touching_mouse():
        snake.say("You caught me!")
    elif dist < 80:
        snake.say("Too close!")
    elif dist < 150:
        snake.say("Getting warmer...")
    else:
        snake.say(f"Distance: {int(dist)}")

    # Slowly move toward mouse
    snake.point_towards_mouse()
    if dist > 10:
        snake.move(2)

Key concepts: distance_to_mouse(), touching_mouse(), point_towards_mouse(), mouse_x(), mouse_y()

Mouse Tracking


User Input (Ask and Answer)

Get text input from the user.

from ektupy import Stage, Sprite, on_start, on_key_press
from ektupy import ask, answer, wait

stage = Stage()
stage.set_background("#4a90d9")

snake = Sprite("snake")
stage.add_sprite(snake)

@snake.on_start
def greet():
    snake.go_to(0, 100)
    snake.size = 100

    # Ask for the user's name
    name = ask("What's your name?")

    # Use the answer
    snake.say(f"Hello, {name}! Nice to meet you!")
    wait(2)

    # Ask another question
    color = ask("What's your favorite color?")
    snake.say(f"{color} is a great color!")
    wait(2)

    snake.say("Press SPACE to ask again!")

@snake.on_key_press("space")
def ask_again():
    response = ask("Tell me something interesting:")
    snake.say(f'You said: "{response}"')

Key concepts: ask(), answer() - the input box appears at the bottom of the stage

User Input


Timer and Clock

Use sensing functions for time.

from ektupy import Stage, Sprite, on_start, on_forever
from ektupy import timer, reset_timer
from ektupy import current_hour, current_minute, current_second, current_day_of_week

stage = Stage()
stage.set_background("#1a1a2e")

clock = Sprite("snake")
stage.add_sprite(clock)

days = ["", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]

@clock.on_start
def setup():
    clock.go_to(0, 100)
    clock.size = 100
    reset_timer()

@clock.on_forever
def update_clock():
    h = current_hour()
    m = current_minute()
    s = current_second()
    day = days[current_day_of_week()]

    # Format time with leading zeros
    time_str = f"{h:02d}:{m:02d}:{s:02d}"
    elapsed = int(timer())

    clock.say(f"{day}\n{time_str}\nRunning: {elapsed}s")

Key concepts: timer(), reset_timer(), current_hour(), current_minute(), current_second(), current_day_of_week()

Timer and Clock


Two Sprites with Independent Controls

Each sprite has its own scripts and responds to different keys.

from ektupy import Stage, Sprite, wait, broadcast

stage = Stage()
stage.set_background("#87CEEB")

cat = Sprite("cat")
dog = Sprite("dog")

stage.add_sprite(cat)
stage.add_sprite(dog)

# ============================================
# CAT'S SCRIPTS - Use arrow keys to move
# ============================================

@cat.on_start
def cat_setup():
    cat.go_to(-100, 0)
    cat.say("I'm the cat! Use arrow keys to move me.")
    wait(3)
    cat.say("")

@cat.on_key_press("up arrow")
def cat_up():
    cat.change_y(10)

@cat.on_key_press("down arrow")
def cat_down():
    cat.change_y(-10)

@cat.on_key_press("left arrow")
def cat_left():
    cat.change_x(-10)

@cat.on_key_press("right arrow")
def cat_right():
    cat.change_x(10)

@cat.on_message("woof")
def cat_hears_woof():
    cat.say("Meow back!")
    wait(1)
    cat.say("")

# ============================================
# DOG'S SCRIPTS - Use WASD keys to move
# ============================================

@dog.on_start
def dog_setup():
    dog.go_to(100, 0)
    dog.say("I'm the dog! Use WASD to move me.")
    wait(3)
    dog.say("")

@dog.on_key_press("w")
def dog_up():
    dog.change_y(10)

@dog.on_key_press("s")
def dog_down():
    dog.change_y(-10)

@dog.on_key_press("a")
def dog_left():
    dog.change_x(-10)

@dog.on_key_press("d")
def dog_right():
    dog.change_x(10)

@dog.on_key_press("space")
def dog_bark():
    dog.say("Woof!")
    broadcast("woof")
    wait(1)
    dog.say("")

# ============================================
# COLLISION DETECTION
# ============================================

@cat.on_forever
def check_collision():
    if cat.touching(dog):
        cat.say("Hey!")
        dog.say("Hi friend!")

Key concepts: Per-sprite decorators (@sprite.on_start, @sprite.on_key_press, @sprite.on_message), touching(), broadcast()

Two Sprites


Cloning

Create multiple copies of a sprite at runtime.

from ektupy import Stage, Sprite, on_start, on_key_press
import random

stage = Stage()
stage.set_background("#1a1a2e")

star = Sprite("snake")
stage.add_sprite(star)

@star.on_start
def star_setup():
    star.hide()
    star.size = 30

def on_clone_created():
    star.show()
    star.go_to_random_position()
    star.set_ghost_effect(random.randint(0, 50))

star.on_clone_start(on_clone_created)

@on_start
def setup():
    print("Press SPACE to create stars!")

@on_key_press("space")
def create_star():
    star.clone()
    print("Created a star!")

Key concepts: clone(), on_clone_start(), is_clone, delete_clone(), go_to_random_position()

Cloning


Click to Move

Click anywhere on the stage to move the sprite there smoothly.

from ektupy import Stage, Sprite, on_start, on_mouse_click

stage = Stage()
sprite = Sprite("snake")
stage.add_sprite(sprite)

@on_start
def setup():
    sprite.go_to(0, 0)
    sprite.say("Click anywhere!")

@on_mouse_click
def move_to_click(x, y):
    sprite.glide_to(x, y, 0.5)
    sprite.say(f"({int(x)}, {int(y)})")

Key concepts: @on_mouse_click, glide_to()

Click to Move


Broadcasting Messages

Use broadcasts to coordinate between sprites.

from ektupy import Stage, Sprite, on_start, on_key_press, on_message, broadcast, wait

stage = Stage()

button = Sprite("snake")
responder = Sprite("cat")

stage.add_sprite(button)
stage.add_sprite(responder)

@button.on_start
def button_setup():
    button.go_to(-100, 0)
    button.say("Press SPACE!")

@responder.on_start
def responder_setup():
    responder.go_to(100, 0)

@on_key_press("space")
def send_message():
    button.say("Sending...")
    broadcast("hello")
    wait(0.5)
    button.say("Press SPACE!")

@responder.on_message("hello")
def receive_message():
    responder.say("Got it!")
    responder.change_y(20)
    wait(0.5)
    responder.change_y(-20)
    responder.say("")

Key concepts: broadcast(), @on_message, broadcast_and_wait()

Broadcasting


Background Images

Use backdrop images for your stage.

from ektupy import Stage, Sprite, on_start, on_forever, key_pressed

stage = Stage()
stage.set_background_image("mountain-sunrise")

snake = Sprite("snake")
stage.add_sprite(snake)

# Available backdrops: mountain-sunrise, jungle, paddy-field, race-track, sky-and-clouds
backdrops = ["mountain-sunrise", "jungle", "paddy-field", "race-track", "sky-and-clouds"]
current_backdrop = 0

@on_start
def setup():
    snake.go_to(0, -200)
    snake.say("Press SPACE to change backdrop!", 3)

@on_forever
def game_loop():
    global current_backdrop

    # Move with arrow keys
    if key_pressed("right arrow"):
        snake.direction = 90
        snake.move(5)
    if key_pressed("left arrow"):
        snake.direction = -90
        snake.move(5)
    if key_pressed("up arrow"):
        snake.direction = 0
        snake.move(5)
    if key_pressed("down arrow"):
        snake.direction = 180
        snake.move(5)

    # Change backdrop with space
    if key_pressed("space"):
        current_backdrop = (current_backdrop + 1) % len(backdrops)
        stage.set_background_image(backdrops[current_backdrop])
        snake.say(backdrops[current_backdrop], 1)

Key concepts: set_background_image(), available backdrops: jungle, mountain-sunrise, paddy-field, race-track, sky-and-clouds

Background Images

EktuPy Examples

This page contains example programs demonstrating EktuPy features. Each example includes a code walkthrough explaining the key concepts.


Hello World

The simplest EktuPy program - display a sprite with a speech bubble.

What you’ll learn:

  • Creating a stage and sprite
  • Using the @on_start decorator
  • Basic sprite positioning and speech bubbles
View Code
# Hello Snake - Simple greeting example

from ektupy import Stage, Sprite, on_start

stage = Stage()
stage.set_background("#2d5a27")

snake = Sprite("snake")
stage.add_sprite(snake)

@on_start
def setup():
    snake.go_to(0, 0)
    snake.say("Hello, I'm Py the Snake!", 5)

Code Breakdown

Imports and Setup

from ektupy import Stage, Sprite, on_start

Import the core classes and the @on_start decorator.

Stage Creation

stage = Stage()
stage.set_background("#2d5a27")  # Dark green color

Create a 960x720 pixel stage with a custom background color using CSS hex notation.

Sprite Creation

snake = Sprite("snake")
stage.add_sprite(snake)

Create a sprite using the asset name “snake” and add it to the stage. The sprite won’t be visible until added.

Event Handler

@on_start
def setup():
    snake.go_to(0, 0)
    snake.say("Hello, I'm Py the Snake!", 5)

The @on_start decorator runs this function once when the program starts. say() displays a speech bubble for 5 seconds.


Arrow Key Movement

Move a sprite around the stage using arrow keys.

What you’ll learn:

  • Using @on_forever for continuous checking
  • Reading keyboard input with key_pressed()
  • Sprite direction and movement
  • Edge bouncing
View Code
# Arrow Key Movement Example
# Use arrow keys to move the snake

from ektupy import Stage, Sprite, on_start, on_forever, key_pressed

stage = Stage()
snake = Sprite("snake")
stage.add_sprite(snake)

speed = 5

@on_start
def setup():
    snake.go_to(0, 0)
    snake.say("Use arrow keys to move!", 3)

@on_forever
def game_loop():
    if key_pressed("right arrow"):
        snake.direction = 90
        snake.move(speed)
    if key_pressed("left arrow"):
        snake.direction = -90
        snake.move(speed)
    if key_pressed("up arrow"):
        snake.direction = 0
        snake.move(speed)
    if key_pressed("down arrow"):
        snake.direction = 180
        snake.move(speed)

    snake.bounce_if_on_edge()

Code Breakdown

Game Variables

speed = 5

Store movement speed in a variable for easy adjustment.

The Forever Loop

@on_forever
def game_loop():

This function runs every frame (~60 times per second), perfect for checking continuous input.

Key Checking

if key_pressed("right arrow"):
    snake.direction = 90    # Point right (90 degrees)
    snake.move(speed)       # Move forward

key_pressed() returns True while a key is held. Direction uses Scratch-style angles: 0=up, 90=right, 180=down, -90=left.

Boundary Handling

snake.bounce_if_on_edge()

Prevents the sprite from leaving the stage by reversing direction at edges.


Bouncing Ball

A sprite that bounces around the stage continuously.

What you’ll learn:

  • Random initial direction
  • Continuous movement with bouncing
  • Using Python’s random module
View Code
# Bouncing Ball Example
# Watch the snake bounce around the stage

from ektupy import Stage, Sprite, on_start, on_forever
import random

stage = Stage()
stage.set_background("#1a1a2e")

snake = Sprite("snake")
stage.add_sprite(snake)

speed = 8

@on_start
def setup():
    snake.go_to(0, 0)
    snake.direction = random.randint(30, 60)
    snake.say("Wheee!", 2)

@on_forever
def game_loop():
    snake.move(speed)
    snake.bounce_if_on_edge()

Code Breakdown

Random Direction

import random
snake.direction = random.randint(30, 60)

Python’s standard library works in EktuPy! Set a random diagonal direction at startup.

Continuous Motion

@on_forever
def game_loop():
    snake.move(speed)
    snake.bounce_if_on_edge()

The sprite moves forward continuously and bounces off walls automatically.


Drawing with Pen

Use the pen to draw on the stage as the sprite moves.

What you’ll learn:

  • Pen down/up for drawing
  • Pen color and size
  • Following mouse position
  • Clearing drawings
View Code
# Drawing with Pen Example
# Click and drag to draw, press 'c' to clear

from ektupy import Stage, Sprite, on_start, on_forever
from ektupy import mouse_x, mouse_y, mouse_down, key_pressed

stage = Stage()
pen = Sprite("snake")
pen.size = 30
stage.add_sprite(pen)

@on_start
def setup():
    pen.pen_up()
    pen.set_pen_color("#00ff00")
    pen.set_pen_size(3)
    pen.say("Click to draw, 'c' to clear", 3)

@on_forever
def draw():
    pen.go_to(mouse_x(), mouse_y())

    if mouse_down():
        pen.pen_down()
    else:
        pen.pen_up()

    # Press 'c' to clear
    if key_pressed("c"):
        pen.clear()

    # Color keys
    if key_pressed("1"):
        pen.set_pen_color("#ff0000")  # Red
    if key_pressed("2"):
        pen.set_pen_color("#00ff00")  # Green
    if key_pressed("3"):
        pen.set_pen_color("#0000ff")  # Blue

Code Breakdown

Pen Setup

pen.pen_up()                    # Start with pen up
pen.set_pen_color("#00ff00")    # Green color
pen.set_pen_size(3)             # 3 pixel line width

Configure pen before drawing. Use CSS colors (hex, rgb, or named).

Mouse Tracking

pen.go_to(mouse_x(), mouse_y())

Move sprite to current mouse position every frame.

Drawing Logic

if mouse_down():
    pen.pen_down()
else:
    pen.pen_up()

Only draw when mouse button is pressed.

Color Switching

if key_pressed("1"):
    pen.set_pen_color("#ff0000")

Change colors with number keys for variety.


Two Sprites

Each sprite can have its own scripts, just like in Scratch!

What you’ll learn:

  • Per-sprite event decorators (@sprite.on_start, @sprite.on_key_press)
  • Multiple sprites with independent controls
  • Collision detection
  • Broadcasting messages between sprites
View Code
# Two Sprites Example - Cat and Dog
# This shows how each sprite can have its own scripts, just like Scratch!

# Create the stage
stage = Stage()
stage.set_background("#87CEEB")  # Sky blue background

# Create our two sprites
cat = Sprite("cat")
dog = Sprite("dog")

# Add sprites to the stage
stage.add_sprite(cat)
stage.add_sprite(dog)

# ============================================
# CAT'S SCRIPTS - Use arrow keys to move
# ============================================

@cat.on_start
def cat_setup():
    cat.go_to(-100, 0)
    cat.say("I'm the cat! Use arrow keys to move me.")
    wait(3)
    cat.say("")

@cat.on_key_press("up arrow")
def cat_up():
    cat.change_y(10)

@cat.on_key_press("down arrow")
def cat_down():
    cat.change_y(-10)

@cat.on_key_press("left arrow")
def cat_left():
    cat.change_x(-10)

@cat.on_key_press("right arrow")
def cat_right():
    cat.change_x(10)

@cat.on_message("woof")
def cat_hears_woof():
    cat.say("Meow back!")
    wait(1)
    cat.say("")

# ============================================
# DOG'S SCRIPTS - Use WASD keys to move
# ============================================

@dog.on_start
def dog_setup():
    dog.go_to(100, 0)
    dog.say("I'm the dog! Use WASD to move me.")
    wait(3)
    dog.say("")

@dog.on_key_press("w")
def dog_up():
    dog.change_y(10)

@dog.on_key_press("s")
def dog_down():
    dog.change_y(-10)

@dog.on_key_press("a")
def dog_left():
    dog.change_x(-10)

@dog.on_key_press("d")
def dog_right():
    dog.change_x(10)

@dog.on_key_press("space")
def dog_bark():
    dog.say("Woof!")
    broadcast("woof")
    wait(1)
    dog.say("")

# ============================================
# COLLISION DETECTION - Check if they touch
# ============================================

@cat.on_forever
def check_collision():
    if cat.touching(dog):
        cat.say("Hey!")
        dog.say("Hi friend!")

Code Breakdown

Per-Sprite Decorators

@cat.on_start       # Only for cat
@dog.on_key_press("w")  # Only for dog

Each sprite has its own event handlers, keeping code organized.

Different Control Schemes

  • Cat uses arrow keys
  • Dog uses WASD keys Both can move simultaneously!

Message Broadcasting

broadcast("woof")           # Dog sends message

@cat.on_message("woof")     # Cat receives it
def cat_hears_woof():
    cat.say("Meow back!")

Sprites can communicate through broadcast messages.

Collision Detection

if cat.touching(dog):
    cat.say("Hey!")

Check if two sprites are overlapping using bounding box collision.


Many Sprites

Click on sprites to select them, then move with arrow keys.

What you’ll learn:

  • Managing multiple sprites
  • Click handlers with on_clicked()
  • Global state for selection
  • Dynamic behavior based on selection
View Code
# Many Sprites Example
# Click on a sprite to select it, then use arrow keys to move it!

from ektupy import Stage, Sprite, on_start, on_key_press
import random

stage = Stage()
stage.set_background("#1a1a2e")

# Create all the sprites
lion = Sprite("lion")
fish = Sprite("fish")
cat = Sprite("cat")
shark = Sprite("shark")
snake = Sprite("snake")

# Add them to the stage
stage.add_sprite(lion)
stage.add_sprite(fish)
stage.add_sprite(cat)
stage.add_sprite(shark)
stage.add_sprite(snake)

# Track which sprite is selected
selected_sprite = None

def select_sprite(sprite, name):
    """Select a sprite and make it say its name."""
    global selected_sprite
    selected_sprite = sprite

    # Clear speech from all sprites
    lion.say("")
    fish.say("")
    cat.say("")
    shark.say("")
    snake.say("")

    # Selected sprite says its name
    sprite.say(f"I'm {name}!")

@on_start
def setup():
    # Place sprites at random positions
    for sprite in [lion, fish, cat, shark, snake]:
        x = random.randint(-350, 350)
        y = random.randint(-250, 250)
        sprite.go_to(x, y)
        sprite.size = 80

    print("Click on a sprite to select it!")
    print("Use arrow keys to move the selected sprite.")

# Click handlers for each sprite
def click_lion():
    select_sprite(lion, "Lion")

def click_fish():
    select_sprite(fish, "Fish")

def click_cat():
    select_sprite(cat, "Cat")

def click_shark():
    select_sprite(shark, "Shark")

def click_snake():
    select_sprite(snake, "Snake")

# Register click handlers
lion.on_clicked(click_lion)
fish.on_clicked(click_fish)
cat.on_clicked(click_cat)
shark.on_clicked(click_shark)
snake.on_clicked(click_snake)

# Arrow key movement
@on_key_press("up arrow")
def move_up():
    if selected_sprite:
        selected_sprite.change_y(20)

@on_key_press("down arrow")
def move_down():
    if selected_sprite:
        selected_sprite.change_y(-20)

@on_key_press("left arrow")
def move_left():
    if selected_sprite:
        selected_sprite.change_x(-20)

@on_key_press("right arrow")
def move_right():
    if selected_sprite:
        selected_sprite.change_x(20)

Code Breakdown

Click Handler Registration

lion.on_clicked(click_lion)

Register a callback function to run when this sprite is clicked.

Selection State

selected_sprite = None

def select_sprite(sprite, name):
    global selected_sprite
    selected_sprite = sprite

Use a global variable to track the currently selected sprite.

Conditional Movement

if selected_sprite:
    selected_sprite.change_y(20)

Only move if a sprite is selected (not None).


Costume Animation

Watch the snake change costumes as it moves!

What you’ll learn:

  • Costume system
  • Frame-based animation timing
  • Accessing costume properties
View Code
# Costume Animation Example
# Watch the snake change colors as it moves!

from ektupy import Stage, Sprite, on_start, on_forever

stage = Stage()
stage.set_background("#2d3436")

# Create a snake sprite - automatically loads all costumes
snake = Sprite("snake")
stage.add_sprite(snake)

frame_count = 0

@on_start
def setup():
    snake.go_to(0, 0)
    snake.set_size(150)
    # Show available costumes
    print("Available costumes:", snake.costumes)
    snake.say("I can change colors!", 2)

@on_forever
def animate():
    global frame_count
    frame_count += 1

    # Move in a circle
    snake.turn_right(3)
    snake.move(5)

    # Change costume every 20 frames
    if frame_count % 20 == 0:
        snake.next_costume()
        print(f"Now wearing: {snake.costume_name}")

Code Breakdown

Costume Access

print("Available costumes:", snake.costumes)

Sprites can have multiple costumes. Access the list via the costumes property.

Frame Counting

frame_count += 1
if frame_count % 20 == 0:
    snake.next_costume()

Use modulo (%) to trigger actions periodically. At 60fps, % 20 triggers ~3 times per second.

Costume Properties

snake.costume_name   # Current costume name
snake.costume_number # Current costume index

Mouse Tracker

A sprite that follows and reacts to the mouse.

What you’ll learn:

  • Distance calculations
  • Mouse sensing
  • Point-towards behavior
  • Conditional responses based on distance
View Code
# Mouse Tracker - Sensing Mouse Position
# Demonstrates distance_to_mouse and touching_mouse

from ektupy import Stage, Sprite, on_start, on_forever
from ektupy import mouse_x, mouse_y

stage = Stage()
stage.set_background("#2d5a27")

snake = Sprite("snake")
stage.add_sprite(snake)

@snake.on_start
def setup():
    snake.go_to(0, 0)
    snake.size = 80

@snake.on_forever
def track_mouse():
    # Calculate distance to mouse
    dist = snake.distance_to_mouse()

    # Check if touching mouse pointer
    if snake.touching_mouse():
        snake.say("You caught me!")
    elif dist < 80:
        snake.say("Too close!")
    elif dist < 150:
        snake.say("Getting warmer...")
    else:
        snake.say(f"Distance: {int(dist)}")

    # Slowly move toward mouse
    snake.point_towards_mouse()
    if dist > 10:
        snake.move(2)

Code Breakdown

Distance Sensing

dist = snake.distance_to_mouse()

Get the pixel distance between sprite center and mouse position.

Conditional Responses

if snake.touching_mouse():
    snake.say("You caught me!")
elif dist < 80:
    snake.say("Too close!")

Create zones of different behavior based on distance.

Following Behavior

snake.point_towards_mouse()
if dist > 10:
    snake.move(2)

Point toward mouse and move only if not already there (prevents jittering).


Ask Name

Get user input through an on-screen text box.

What you’ll learn:

  • Using ask() for user input
  • Working with the returned answer
  • Sequential prompts
View Code
# Ask Your Name - User Input Example
# Demonstrates the ask() and answer() functions

from ektupy import Stage, Sprite, on_start, on_key_press
from ektupy import ask, answer, wait

stage = Stage()
stage.set_background("#4a90d9")

snake = Sprite("snake")
stage.add_sprite(snake)

@snake.on_start
def greet():
    snake.go_to(0, 100)
    snake.size = 100

    # Ask for the user's name
    name = ask("What's your name?")

    # Use the answer
    snake.say(f"Hello, {name}! Nice to meet you!")
    wait(2)

    # Ask another question
    color = ask("What's your favorite color?")
    snake.say(f"{color} is a great color!")
    wait(2)

    snake.say("Press SPACE to ask again!")

@snake.on_key_press("space")
def ask_again():
    response = ask("Tell me something interesting:")
    snake.say(f'You said: "{response}"')

Code Breakdown

Getting Input

name = ask("What's your name?")

Shows an input box at the bottom of the stage. Program pauses until user submits.

Using the Answer

snake.say(f"Hello, {name}!")

The returned value is a string you can use immediately.

Multiple Questions

name = ask("What's your name?")
color = ask("What's your favorite color?")

Each ask() waits for its own response before continuing.


Digital Clock

Display the current time and date using sensing functions.

What you’ll learn:

  • Date/time functions
  • Timer functions
  • String formatting
  • Creating a live display
View Code
# Digital Clock - Sensing Time Example
# Demonstrates timer and date/time functions

from ektupy import Stage, Sprite, on_start, on_forever
from ektupy import timer, reset_timer
from ektupy import current_hour, current_minute, current_second, current_day_of_week

stage = Stage()
stage.set_background("#1a1a2e")

clock = Sprite("snake")
stage.add_sprite(clock)

days = ["", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]

@clock.on_start
def setup():
    clock.go_to(0, 100)
    clock.size = 100
    reset_timer()

@clock.on_forever
def update_clock():
    h = current_hour()
    m = current_minute()
    s = current_second()
    day = days[current_day_of_week()]

    # Format time with leading zeros
    time_str = f"{h:02d}:{m:02d}:{s:02d}"
    elapsed = int(timer())

    clock.say(f"{day}\n{time_str}\nRunning: {elapsed}s")

Code Breakdown

Time Functions

h = current_hour()      # 0-23
m = current_minute()    # 0-59
s = current_second()    # 0-59

Get current time from the user’s system.

Day of Week

days = ["", "Sunday", "Monday", ...]
day = days[current_day_of_week()]  # 1=Sunday

Convert numeric day (1-7) to a name using a lookup list.

String Formatting

f"{h:02d}:{m:02d}:{s:02d}"  # "09:05:03"

Python f-strings with format specifiers for leading zeros.

Timer

reset_timer()           # Start at 0
elapsed = int(timer())  # Seconds since reset

Track time elapsed since program started.


Background Images

Use backdrop images and cycle through them.

What you’ll learn:

  • Setting background images
  • Discovering available assets
  • Cycling through options
View Code
# Background Image Example
# Demonstrates using backdrop images with sprites

from ektupy import Stage, Sprite, on_start, on_forever, key_pressed
from ektupy import get_available_backdrops

stage = Stage()
stage.set_background_image("mountain-sunrise")

snake = Sprite("snake")
stage.add_sprite(snake)

# Track current backdrop index
backdrops = ["mountain-sunrise", "jungle", "paddy-field", "sky-and-clouds"]
current_backdrop = 0

@on_start
def setup():
    snake.go_to(0, -200)
    snake.say("Press SPACE to change backdrop!", 3)
    print("Available backdrops:", get_available_backdrops())

@on_forever
def game_loop():
    global current_backdrop

    # Move with arrow keys
    if key_pressed("right arrow"):
        snake.direction = 90
        snake.move(5)
    if key_pressed("left arrow"):
        snake.direction = -90
        snake.move(5)
    if key_pressed("up arrow"):
        snake.direction = 0
        snake.move(5)
    if key_pressed("down arrow"):
        snake.direction = 180
        snake.move(5)

    # Change backdrop with space
    if key_pressed("space"):
        current_backdrop = (current_backdrop + 1) % len(backdrops)
        stage.set_background_image(backdrops[current_backdrop])
        snake.say(backdrops[current_backdrop], 1)

Code Breakdown

Setting Background Image

stage.set_background_image("mountain-sunrise")

Use asset names for backgrounds, just like sprites.

Asset Discovery

print("Available backdrops:", get_available_backdrops())

Runtime function to list available backdrop names.

Cycling with Modulo

current_backdrop = (current_backdrop + 1) % len(backdrops)

Wrap around to 0 when reaching the end of the list.


Pong Game

A complete two-player Pong game with sound effects.

What you’ll learn:

  • Game state management
  • Two-player controls
  • Ball physics with bouncing
  • Score tracking
  • Sound effects
View Code
# Pong Game Example
# Classic two-player Pong game
# Player 1 (Left): W/S keys
# Player 2 (Right): Up/Down arrow keys
# Press SPACE to start/restart the ball

# Game variables (scaled for 960x720 stage)
ball_speed_x = 0
ball_speed_y = 0
paddle_speed = 10
score1 = 0
score2 = 0
ball_moving = False

# Create the stage with dark background
stage = Stage()
stage.set_background("#1a1a2e")

# Load sound effects
load_sound("hit", "sfx_sound_neutral1")
load_sound("score", "sfx_sound_neutral5")

# Create sprites
paddle1 = Sprite("paddle")
paddle2 = Sprite("paddle")
ball = Sprite("ball")

# Add sprites to stage
stage.add_sprite(paddle1)
stage.add_sprite(paddle2)
stage.add_sprite(ball)

@on_start
def setup():
    global ball_speed_x, ball_speed_y, score1, score2, ball_moving

    # Position paddles on left and right sides
    paddle1.go_to(-440, 0)
    paddle2.go_to(440, 0)

    # Center the ball
    ball.go_to(0, 0)
    ball.size = 80

    # Reset scores and ball state
    score1 = 0
    score2 = 0
    ball_speed_x = 0
    ball_speed_y = 0
    ball_moving = False

    # Show instructions
    ball.say("Press SPACE to start!")

@on_key_press("space")
def start_ball():
    global ball_speed_x, ball_speed_y, ball_moving

    if not ball_moving:
        ball_speed_x = 6
        ball_speed_y = 4
        ball_moving = True
        ball.say("")

@on_forever
def game_loop():
    global ball_speed_x, ball_speed_y, score1, score2, ball_moving

    # Player 1 controls (W/S)
    if key_pressed("w"):
        if paddle1.y < 280:
            paddle1.change_y(paddle_speed)
    if key_pressed("s"):
        if paddle1.y > -280:
            paddle1.change_y(-paddle_speed)

    # Player 2 controls (Up/Down arrows)
    if key_pressed("up arrow"):
        if paddle2.y < 280:
            paddle2.change_y(paddle_speed)
    if key_pressed("down arrow"):
        if paddle2.y > -280:
            paddle2.change_y(-paddle_speed)

    # Only move ball if game is active
    if not ball_moving:
        return

    # Move the ball
    ball.change_x(ball_speed_x)
    ball.change_y(ball_speed_y)

    # Ball bounces off top and bottom walls
    if ball.y > 330 or ball.y < -330:
        ball_speed_y = -ball_speed_y

    # Ball bounces off paddles
    if ball.touching(paddle1) and ball_speed_x < 0:
        ball_speed_x = -ball_speed_x
        ball_speed_y += (ball.y - paddle1.y) * 0.1
        start_sound("hit")

    if ball.touching(paddle2) and ball_speed_x > 0:
        ball_speed_x = -ball_speed_x
        ball_speed_y += (ball.y - paddle2.y) * 0.1
        start_sound("hit")

    # Limit vertical speed
    if ball_speed_y > 16:
        ball_speed_y = 16
    if ball_speed_y < -16:
        ball_speed_y = -16

    # Score when ball goes past paddles
    if ball.x < -480:
        score2 += 1
        reset_ball()

    if ball.x > 480:
        score1 += 1
        reset_ball()

def reset_ball():
    global ball_speed_x, ball_speed_y, ball_moving

    start_sound("score")
    ball.go_to(0, 0)
    ball_speed_x = 0
    ball_speed_y = 0
    ball_moving = False
    ball.say(f"{score1} - {score2}  Press SPACE")

Code Breakdown

Game State

ball_speed_x = 0
ball_speed_y = 0
ball_moving = False
score1 = 0
score2 = 0

Track all game state in global variables.

Sound Effects

load_sound("hit", "sfx_sound_neutral1")
start_sound("hit")  # Play when ball hits paddle

Load sounds with asset names, play them at appropriate moments.

Paddle Movement with Bounds

if key_pressed("w"):
    if paddle1.y < 280:  # Don't go off top
        paddle1.change_y(paddle_speed)

Check bounds before moving to keep paddles on screen.

Ball Physics

# Bounce off walls
if ball.y > 330 or ball.y < -330:
    ball_speed_y = -ball_speed_y

# Bounce off paddle with angle
if ball.touching(paddle1) and ball_speed_x < 0:
    ball_speed_x = -ball_speed_x
    ball_speed_y += (ball.y - paddle1.y) * 0.1  # Add spin

Simple physics with direction reversal and spin based on hit position.

Scoring

if ball.x < -480:
    score2 += 1
    reset_ball()

Check if ball passes paddles to award points.


Racing Game

Avoid traffic in this endless racing game.

What you’ll learn:

  • Object pooling for traffic
  • Lane-based movement
  • Collision detection
  • Background music with looping
  • Progressive difficulty
View Code
"""
Racing Down - Avoid the Traffic!

Use LEFT and RIGHT arrow keys to move your red car
Avoid the oncoming traffic. Don't crash!
Press SPACE to restart after game over
"""
from ektupy import (
    Stage, Sprite, on_start, on_forever, on_key_press,
    load_sound, start_sound, set_sound_loop, set_sound_volume
)
import random

# Create stage with race track background
stage = Stage()
stage.set_background_image("race-track")

# Player's red car
player = Sprite("car")
player.switch_costume("red")
player.size = 80
stage.add_sprite(player)

# Traffic vehicles pool
traffic_types = [
    ("car", "blue", 70),
    ("car", "green", 70),
    ("car", "default", 70),
    ("truck", "yellow", 80),
    ("truck", "default", 80),
]
traffic = []

# Create pool of traffic vehicles
for i in range(6):
    sprite_name, costume_name, size = traffic_types[i % 3]
    car = Sprite(sprite_name)
    car.switch_costume(costume_name)
    car.size = size
    car.hide()
    stage.add_sprite(car)
    traffic.append(car)

# Lane positions (x coordinates for 3 lanes)
LANES = [-130, 0, 130]

# Game state
game_over = False
score = 0
speed = 5

# Load sounds
load_sound("crash", "sfx_damage_hit1")
load_sound("music", "boss_battle")


@on_start
def setup():
    global game_over, score, speed
    game_over = False
    score = 0
    speed = 5

    player.go_to(0, -280)
    player.show()

    for car in traffic:
        car.hide()
        car.go_to(0, 500)

    # Start background music
    set_sound_loop("music", True)
    set_sound_volume("music", 30)
    start_sound("music")


def spawn_traffic():
    for car in traffic:
        if not car.visible:
            lane = random.choice(LANES)
            car.go_to(lane, 400)
            car.show()
            return


def check_collision(car):
    px, py = player.x, player.y
    cx, cy = car.x, car.y
    player_w, player_h = 30, 50
    car_w, car_h = 30, 50

    if abs(px - cx) < (player_w + car_w) / 2:
        if abs(py - cy) < (player_h + car_h) / 2:
            return True
    return False


@on_forever
def game_loop():
    global game_over, score, speed

    if game_over:
        return

    for car in traffic:
        if car.visible:
            car.change_y(-speed)

            if check_collision(car):
                game_over = True
                start_sound("crash")
                player.say("GAME OVER!")
                return

            if car.y < -400:
                car.hide()
                score += 1

    # Spawn new traffic (max 3 at a time)
    if count_active_traffic() < 3:
        if random.random() < 0.03:
            spawn_traffic()

    # Gradually increase speed
    if score > 0 and score % 10 == 0:
        speed = min(12, 5 + score // 10)


@on_key_press("left arrow")
def move_left():
    if not game_over:
        current_x = player.x
        for i, lane in enumerate(LANES):
            if abs(current_x - lane) < 50:
                if i > 0:
                    player.set_x(LANES[i - 1])
                break


@on_key_press("right arrow")
def move_right():
    if not game_over:
        current_x = player.x
        for i, lane in enumerate(LANES):
            if abs(current_x - lane) < 50:
                if i < len(LANES) - 1:
                    player.set_x(LANES[i + 1])
                break


@on_key_press("space")
def restart_game():
    global game_over, score, speed
    if game_over:
        game_over = False
        score = 0
        speed = 5
        player.go_to(0, -280)
        player.say("")
        for car in traffic:
            car.hide()

Code Breakdown

Object Pooling

traffic = []
for i in range(6):
    car = Sprite(sprite_name)
    car.hide()
    traffic.append(car)

Pre-create sprites and reuse them instead of creating new ones. Improves performance.

Lane-Based Movement

LANES = [-130, 0, 130]
for i, lane in enumerate(LANES):
    if abs(current_x - lane) < 50:
        if i > 0:
            player.set_x(LANES[i - 1])

Snap player to discrete lanes instead of free movement.

Background Music

set_sound_loop("music", True)   # Loop the music
set_sound_volume("music", 30)   # 30% volume
start_sound("music")            # Start playing

Configure and play looping background music.

Progressive Difficulty

if score > 0 and score % 10 == 0:
    speed = min(12, 5 + score // 10)

Increase traffic speed every 10 points, capped at 12.


Summary

These examples demonstrate the core features of EktuPy:

CategoryExamples
BasicsHello World, Movement
AnimationBouncing Ball, Costumes
InputArrow Keys, Mouse Tracker, Ask Name
DrawingPen Drawing
Multiple SpritesTwo Sprites, Many Sprites
SensingClock, Mouse Tracker
GamesPong, Racing

Try modifying these examples to learn more. Experiment with:

  • Changing colors and speeds
  • Adding more sprites
  • Creating new game mechanics
  • Combining features from different examples

EktuPy API Reference

Complete reference for all EktuPy classes, methods, and functions.

Table of Contents


Stage Class

The Stage is the canvas where all sprites live and move. The stage has a fixed size of 960x720 pixels.

Constructor

Stage(background_color="#ffffff")
ParameterTypeDefaultDescription
background_colorstr“#ffffff”Background color (CSS color)
# Create a stage with default white background
stage = Stage()

# Create a stage with a custom background color
stage = Stage("#1a1a2e")

Properties

PropertyTypeDescription
widthintStage width (always 960)
heightintStage height (always 720)

Coordinate System

EktuPy uses a Scratch-like coordinate system with the origin at the center:

                    (0, 360) Top
                        |
                        |
(-480, 0) Left -----(0, 0)-----> (480, 0) Right
                        |
                        |
                   (0, -360) Bottom
AxisRangeDescription
X-480 to 480Horizontal position (negative = left, positive = right)
Y-360 to 360Vertical position (negative = down, positive = up)

Direction angles:

  • 0 = Up
  • 90 = Right
  • 180 (or -180) = Down
  • -90 = Left

Methods

set_background(color)

Set the stage background color.

stage.set_background("#87CEEB")  # Sky blue
stage.set_background("red")      # Named color

set_background_image(name_or_path)

Set a background image by asset name or path.

# Using an asset name
stage.set_background_image("sky-and-clouds")

# Using a path
stage.set_background_image("/static/assets/backdrops/jungle.svg")

Available backdrops:

  • jungle - Dense jungle forest scene
  • mountain-sunrise - Mountains with rising sun
  • paddy-field - Sun over rice paddy fields
  • race-track - Racing track scene
  • sky-and-clouds - Blue sky with fluffy clouds

add_sprite(sprite)

Add a sprite to the stage.

cat = Sprite("cat")
stage.add_sprite(cat)

start()

Start the game loop. Usually called automatically.

stop()

Stop the game loop.


Sprite Class

Sprites are the visual objects that move and interact on the stage.

Constructor

Sprite(image_path=None)
ParameterTypeDefaultDescription
image_pathstrNoneAsset name or path to sprite image (SVG, PNG, etc.)

You can create sprites using either an asset name (simpler) or a full path:

# Using asset name (recommended)
snake = Sprite("snake")    # Looks up "snake" in the asset map
paddle = Sprite("paddle")  # Looks up "paddle" in the asset map

# Using full path (also works)
snake = Sprite("/static/assets/sprites/snake.svg")

Asset names are easier to use and will show a warning in the editor if the name doesn’t exist. See Asset Map for more details.

Sprite Properties

All properties can be read and written:

PropertyTypeDescription
xfloatX position (0 = center)
yfloatY position (0 = center)
directionfloatDirection in degrees (0=up, 90=right)
sizefloatSize as percentage (100 = normal)
costume_numberintCurrent costume number (read-only)
costume_namestrCurrent costume name (read-only)
is_cloneboolTrue if this sprite is a clone (read-only)
sprite.x = 100
sprite.y = -50
sprite.direction = 90  # Face right
sprite.size = 150      # 150% size
print(sprite.x)        # Read position

Motion Methods

move(steps)

Move in the current direction.

sprite.move(10)  # Move 10 steps forward

turn_right(degrees) / turn_left(degrees)

Rotate the sprite.

sprite.turn_right(90)  # Turn 90 degrees clockwise
sprite.turn_left(45)   # Turn 45 degrees counter-clockwise

go_to(x, y)

Instantly move to a position.

sprite.go_to(0, 0)      # Go to center
sprite.go_to(-100, 50)  # Go to specific position

set_x(x) / set_y(y)

Set just the x or y position.

sprite.set_x(100)
sprite.set_y(-50)

change_x(amount) / change_y(amount)

Change position by an amount.

sprite.change_x(10)   # Move 10 pixels right
sprite.change_y(-5)   # Move 5 pixels down

go_to_random_position()

Move to a random position on the stage.

sprite.go_to_random_position()

go_to_mouse()

Move to the mouse pointer position.

sprite.go_to_mouse()

go_to_sprite(other)

Move to another sprite’s position.

cat.go_to_sprite(dog)

glide_to(x, y, seconds)

Smoothly glide to a position over time.

sprite.glide_to(100, 50, 2)  # Glide over 2 seconds

glide_to_random_position(seconds)

Glide to a random position.

sprite.glide_to_random_position(1.5)

glide_to_mouse(seconds)

Glide to the mouse pointer.

sprite.glide_to_mouse(1)

glide_to_sprite(other, seconds)

Glide to another sprite.

cat.glide_to_sprite(dog, 2)

point_towards(x, y)

Point toward a position.

sprite.point_towards(100, 100)

point_towards_mouse()

Point toward the mouse pointer.

sprite.point_towards_mouse()

point_towards_sprite(other)

Point toward another sprite.

cat.point_towards_sprite(dog)

set_rotation_style(style)

Set how the sprite rotates visually.

StyleDescription
"all around"Rotate freely in all directions
"left-right"Only flip horizontally (default)
"don't rotate"Never rotate visually
sprite.set_rotation_style("all around")

bounce_if_on_edge()

If touching edge, bounce (reverse direction).

@on_forever
def move():
    sprite.move(5)
    sprite.bounce_if_on_edge()

Looks Methods

say(text, seconds=None)

Show a speech bubble.

sprite.say("Hello!")           # Show until changed
sprite.say("Hi!", 2)           # Show for 2 seconds
sprite.say("")                 # Clear bubble

think(text, seconds=None)

Show a thought bubble (cloud shape).

sprite.think("Hmm...")
sprite.think("I wonder...", 3)

show() / hide()

Show or hide the sprite.

sprite.hide()
wait(1)
sprite.show()

change_size(amount)

Change size by amount.

sprite.change_size(10)   # Grow by 10%
sprite.change_size(-20)  # Shrink by 20%

set_ghost_effect(value)

Set transparency (0=visible, 100=invisible).

sprite.set_ghost_effect(50)  # 50% transparent

change_ghost_effect(amount)

Change transparency by amount.

sprite.change_ghost_effect(10)  # More transparent

clear_graphic_effects()

Reset all visual effects.

sprite.clear_graphic_effects()

switch_costume(name_or_index)

Switch to a different costume.

sprite.switch_costume(0)       # By index
sprite.switch_costume("walk1") # By name

next_costume()

Switch to the next costume (wraps around).

sprite.next_costume()

add_costume(image_path, name)

Add a new costume from an image.

sprite.add_costume("/static/assets/walk2.png", "walk2")

Layer Methods

sprite.go_to_front_layer()    # Move to front
sprite.go_to_back_layer()     # Move to back
sprite.go_forward_layers(2)   # Move forward 2 layers
sprite.go_backward_layers(1)  # Move backward 1 layer

Pen Methods

Draw lines on the stage as the sprite moves.

pen_down() / pen_up()

Start or stop drawing.

sprite.pen_down()
sprite.move(100)   # Draws a line
sprite.pen_up()
sprite.move(50)    # No line drawn

set_pen_color(color)

Set the pen color.

sprite.set_pen_color("red")
sprite.set_pen_color("#FF5500")
sprite.set_pen_color("rgb(255, 100, 0)")

set_pen_size(size)

Set the pen width in pixels.

sprite.set_pen_size(5)

stamp()

Stamp the sprite’s image onto the stage.

sprite.stamp()

clear()

Erase all pen drawings.

sprite.clear()

Sensing Methods

touching(other)

Check if touching another sprite.

if cat.touching(dog):
    cat.say("Ouch!")

touching_edge()

Check if touching the stage edge.

if sprite.touching_edge():
    sprite.bounce_if_on_edge()

touching_mouse()

Check if touching the mouse pointer.

if sprite.touching_mouse():
    sprite.say("You're on me!")

distance_to(other)

Get distance to another sprite.

dist = cat.distance_to(dog)
if dist < 50:
    cat.say("Too close!")

distance_to_mouse()

Get distance to the mouse pointer.

dist = sprite.distance_to_mouse()
if dist < 100:
    sprite.say("Getting close!")

Clone Methods

Create copies of sprites at runtime.

clone()

Create a clone of this sprite.

new_clone = sprite.clone()

on_clone_start(callback)

Register a function to run when this sprite is cloned.

def on_cloned():
    # 'sprite' here refers to the new clone
    sprite.go_to_random_position()

sprite.on_clone_start(on_cloned)

delete_clone()

Delete this clone (only works on clones).

if sprite.is_clone:
    sprite.delete_clone()

Per-Sprite Event Decorators

Each sprite can have its own event handlers, just like Scratch.

@sprite.on_start

Runs when the program starts (for this sprite only).

@cat.on_start
def cat_setup():
    cat.go_to(-100, 0)
    cat.say("I'm the cat!")

@sprite.on_forever

Runs every frame (for this sprite only).

@cat.on_forever
def cat_loop():
    cat.turn_right(1)

@sprite.on_key_press(key)

Runs when a key is pressed (for this sprite only).

@cat.on_key_press("space")
def cat_jump():
    cat.change_y(50)

@sprite.on_message(message)

Runs when a broadcast is received (for this sprite only).

@cat.on_message("game_over")
def cat_dies():
    cat.say("Oh no!")
    cat.hide()

sprite.on_clicked(callback)

Runs when this sprite is clicked.

def on_cat_click():
    cat.say("You clicked me!")

cat.on_clicked(on_cat_click)

Global Event Decorators

These decorators register handlers that aren’t tied to a specific sprite.

@on_start

Runs once when the program starts.

@on_start
def setup():
    print("Game started!")

@on_forever

Runs every frame (approximately 60 times per second).

@on_forever
def game_loop():
    # Update game state
    pass

@on_key_press(key)

Runs when a specific key is pressed.

Key names: "a" to "z", "0" to "9", "space", "up arrow", "down arrow", "left arrow", "right arrow", "any" (any key)

@on_key_press("space")
def jump():
    player.change_y(50)

@on_key_press("any")
def any_key(key):
    print(f"Pressed: {key}")

@on_mouse_click

Runs when the mouse is clicked on the stage.

@on_mouse_click
def click_handler(x, y):
    print(f"Clicked at ({x}, {y})")
    sprite.go_to(x, y)

@on_message(message)

Runs when a broadcast message is received.

@on_message("level_complete")
def next_level():
    print("Loading next level...")

Control Functions

wait(seconds)

Pause execution without freezing the program.

@on_start
def setup():
    sprite.say("Hello!")
    wait(2)
    sprite.say("Goodbye!")

wait_until(condition)

Wait until a condition becomes true.

@on_start
def setup():
    sprite.say("Waiting for you to press space...")
    wait_until(lambda: key_pressed("space"))
    sprite.say("Thanks!")

stop() / stop_all()

Stop the game/program.

if game_over:
    stop()

Input Functions

key_pressed(key)

Check if a key is currently held down.

if key_pressed("right arrow"):
    sprite.change_x(5)

mouse_x() / mouse_y()

Get the mouse position on the stage.

x = mouse_x()
y = mouse_y()
sprite.go_to(x, y)

mouse_down()

Check if the mouse button is pressed.

if mouse_down():
    sprite.stamp()

Sensing Functions

Global functions for sensing time, date, and environment.

Timer

timer()

Get the time in seconds since the program started (or since the last reset).

t = timer()
if t > 30:
    sprite.say("30 seconds passed!")

reset_timer()

Reset the timer back to 0.

reset_timer()
# timer() now returns 0

Date and Time

current_year()

Get the current year.

year = current_year()  # e.g., 2025

current_month()

Get the current month (1-12).

month = current_month()  # 1=January, 12=December

current_date()

Get the current day of the month (1-31).

day = current_date()

current_day_of_week()

Get the current day of the week (1-7, where 1=Sunday).

day_of_week = current_day_of_week()
if day_of_week == 1:
    sprite.say("It's Sunday!")

current_hour()

Get the current hour (0-23).

hour = current_hour()
if hour < 12:
    sprite.say("Good morning!")

current_minute()

Get the current minute (0-59).

minute = current_minute()

current_second()

Get the current second (0-59).

second = current_second()

days_since_2000()

Get the number of days since January 1, 2000 (including fractional days).

days = days_since_2000()
sprite.say(f"{int(days)} days since Y2K!")

Ask and Answer

Get user input through an on-screen text box, just like Scratch’s “ask [] and wait” block.

ask(prompt)

Show a text input box at the bottom of the stage with a prompt, wait for the user to type and submit an answer, then return the answer as a string.

@on_start
def setup():
    name = ask("What is your name?")
    sprite.say(f"Hello, {name}!")

The input box appears at the bottom of the stage with:

  • The prompt text displayed above the input field
  • A text input where the user can type
  • A submit button (or press Enter to submit)

The program pauses until the user submits their answer.

answer()

Get the most recent answer from ask(). This is useful when you need to access the last answer later in your code.

@on_start
def setup():
    ask("What is your name?")
    name = answer()  # Get the answer that was just entered

    ask("How old are you?")
    age = answer()   # Get this new answer

    sprite.say(f"Hi {name}, you are {age} years old!")

Example: Multiple questions

@on_start
def quiz():
    score = 0

    ask("What is 5 + 3?")
    if answer() == "8":
        score += 1
        sprite.say("Correct!")
    else:
        sprite.say("Wrong! It's 8")
    wait(1)

    ask("What color is the sky?")
    if answer().lower() == "blue":
        score += 1
        sprite.say("Correct!")
    else:
        sprite.say("Wrong!")
    wait(1)

    sprite.say(f"You scored {score}/2!")

Messaging

broadcast(message, data=None)

Send a message to all listeners.

broadcast("game_over")
broadcast("score_changed", {"score": 100})

Receive with @on_message or @sprite.on_message:

@on_message("score_changed")
def update_score(data):
    print(f"New score: {data['score']}")

broadcast_and_wait(message, data=None)

Send a message and wait for all handlers to complete before continuing.

# Send message and wait for all handlers to finish
broadcast_and_wait("prepare_level")
# This line runs after ALL @on_message("prepare_level") handlers complete
print("Level ready!")

Useful for synchronization:

@cat.on_key_press("space")
def start_animation():
    cat.say("Ready...")
    broadcast_and_wait("all_sprites_ready")  # Wait for everyone
    cat.say("Go!")  # Only runs after all handlers finish

@dog.on_message("all_sprites_ready")
def dog_prepare():
    dog.go_to(0, 0)
    wait(1)  # This delay is waited for by broadcast_and_wait

Console Output

Use standard Python print() for debugging:

print("Hello!")           # Appears in white
print(f"Position: {x}")   # String formatting works

import sys
sys.stderr.write("Error!\n")  # Appears in red

Sound Functions

Play sounds and music in your projects, just like Scratch’s Sound blocks.

Loading and Playing Sounds

load_sound(name, src=None)

Load a sound file and give it a name. Can be called two ways:

Using asset name (recommended):

# Single argument - name is looked up in the asset map
load_sound("sfx_sound_neutral1")  # Loads from asset map

Using explicit path:

# Two arguments - provide alias and path
load_sound("hit", "/static/assets/sounds/hit.mp3")
load_sound("pop", "/static/assets/sounds/pop.wav")
ParameterTypeDescription
namestrAsset name (if single arg) or alias to reference this sound
srcstrOptional. Path or asset name for the sound file (MP3, WAV, OGG)

Asset names are easier to use and will show a warning in the editor if the name doesn’t exist. See Asset Map for more details.

start_sound(name)

Start playing a sound (like Scratch’s “play sound” block). The sound plays in the background while your code continues running.

start_sound("meow")  # Plays and continues immediately
sprite.say("Meow!")  # Runs right away

play_sound_until_done(name)

Play a sound and wait until it finishes (like Scratch’s “play sound until done” block).

@on_start
def setup():
    sprite.say("Listen...")
    play_sound_until_done("meow")  # Waits for sound to finish
    sprite.say("Done!")            # Only runs after sound ends

stop_all_sounds()

Stop all currently playing sounds.

stop_all_sounds()

Volume Control

Control the global volume of all sounds (0-100, like Scratch).

set_volume(value)

Set the volume (0-100).

set_volume(50)   # 50% volume
set_volume(100)  # Full volume
set_volume(0)    # Muted

change_volume(amount)

Change the volume by an amount.

change_volume(10)   # Volume up by 10
change_volume(-20)  # Volume down by 20

get_volume()

Get the current volume level (0-100).

vol = get_volume()
sprite.say(f"Volume: {vol}%")

Sound Effects

Apply effects to sounds like pitch and stereo panning.

set_pitch_effect(value)

Set the pitch effect (like Scratch). Range is approximately -360 to 360.

  • 0 = normal pitch
  • Positive values = higher pitch (faster playback)
  • Negative values = lower pitch (slower playback)
set_pitch_effect(60)   # Higher pitch
set_pitch_effect(-60)  # Lower pitch
set_pitch_effect(0)    # Normal

change_pitch_effect(amount)

Change the pitch effect by an amount.

change_pitch_effect(10)   # Increase pitch
change_pitch_effect(-20)  # Decrease pitch

set_pan_effect(value)

Set the stereo pan (like Scratch’s “set pan left/right” effect).

  • -100 = full left speaker
  • 0 = center (both speakers)
  • 100 = full right speaker
set_pan_effect(-100)  # Full left
set_pan_effect(0)     # Center
set_pan_effect(100)   # Full right

change_pan_effect(amount)

Change the pan effect by an amount.

change_pan_effect(50)  # Pan more to the right

clear_sound_effects()

Reset all sound effects (pitch and pan) to normal.

clear_sound_effects()  # Reset to normal pitch and center pan

Example: Playing Sounds with Effects

stage = Stage()
cat = Sprite("cat")
stage.add_sprite(cat)

# Load sounds using asset names
load_sound("sfx_coin_single1")
load_sound("sfx_sounds_fanfare1")

@on_start
def setup():
    set_volume(70)  # 70% volume
    start_sound("sfx_sounds_fanfare1")  # Play a fanfare

@on_key_press("space")
def coin():
    cat.say("Coin!")
    start_sound("sfx_coin_single1")

@on_key_press("up arrow")
def volume_up():
    change_volume(10)
    cat.say(f"Volume: {get_volume()}%")

@on_key_press("down arrow")
def volume_down():
    change_volume(-10)
    cat.say(f"Volume: {get_volume()}%")

@on_key_press("s")
def stop_sounds():
    stop_all_sounds()
    cat.say("Stopped!")

Asset Map

EktuPy includes an asset map that makes it easy to use sprites and sounds by name instead of remembering full paths.

How It Works

The asset map is a JSON file (asset_map.json) that maps asset names to their full paths. This is generated from the static/assets/sprites and static/assets/sounds directories.

When you use an asset name (without slashes), EktuPy automatically looks it up in the asset map and uses the full path.

Using Asset Names

Sprites:

# Instead of using the full path:
snake = Sprite("/static/assets/sprites/snake.svg")

# You can use the asset name:
snake = Sprite("snake")

Sounds:

# Instead of:
load_sound("hit", "/static/assets/sounds/essential_retro/General_Sounds/Neutral_Sounds/sfx_sound_neutral1.wav")

# You can use:
load_sound("sfx_sound_neutral1")  # Single argument - looked up by name
# or
load_sound("hit", "sfx_sound_neutral1")  # Named alias with asset name

Benefits

  1. Shorter code: No need to type long paths
  2. Editor validation: The editor shows a warning if you use a name that doesn’t exist
  3. Easier to remember: Use descriptive names instead of file paths

Helper Functions

get_available_sprites()

Get a list of all available sprite asset names.

sprites = get_available_sprites()
print(sprites)  # ['snake', 'paddle', 'ball-solid', ...]

get_available_sounds()

Get a list of all available sound asset names.

sounds = get_available_sounds()
print(sounds)  # ['sfx_sound_neutral1', 'sfx_sound_neutral2', ...]

get_available_backdrops()

Get a list of all available backdrop asset names.

backdrops = get_available_backdrops()
print(backdrops)  # ['jungle', 'mountain-sunrise', 'paddy-field', 'race-track', 'sky-and-clouds']

Naming Conflicts

If two files have the same basename but different extensions (e.g., ball.svg and ball.png), they are differentiated by adding the extension to the name:

  • ball-svg for ball.svg
  • ball-png for ball.png

Editor Settings

EktuPy’s editor includes several customizable settings to enhance your coding experience. Access settings by clicking the Settings button in the top navigation bar.

Theme

Switch between light and dark themes for comfortable coding in any environment.

ThemeDescription
LightBright background with dark text (default)
DarkDark background with light text, easier on eyes in low-light

Your theme preference is saved automatically and persists across sessions.


Enable LSP (Language Server Protocol)

The LSP toggle controls syntax checking and code intelligence features.

  • Default: Off
  • Saved to: localStorage (key: ektupy-lsp)

When LSP is OFF

  • No syntax error markers
  • No lint gutter (red/yellow dots)
  • No autocomplete suggestions
  • Faster initial page load

When LSP is ON

  • Real-time syntax checking as you type
  • Red underlines for syntax errors
  • Error markers in the gutter
  • Autocomplete suggestions (Ctrl+Space or as-you-type)
  • Hover information on functions and methods

Why OFF by Default?

  1. Performance: The syntax checker (Pyodide, ~15MB) loads in the background
  2. Simplicity: Beginners may find error markers distracting while learning
  3. Choice: Advanced users can enable it when they want syntax feedback

Type Checking (ty)

The type checking toggle enables advanced static type analysis using ty (Astral’s Python type checker).

  • Default: Off
  • Saved to: localStorage (key: ektupy-typecheck)
  • Requires: LSP must be enabled first

When Type Checking is OFF

Uses the basic syntax-worker (Pyodide) which detects:

  • Syntax errors (missing colons, parentheses, etc.)
  • Invalid sprite/sound names
  • Invalid key names in @on_key_press

When Type Checking is ON

Uses the ty-worker (ty_wasm) which adds:

  • Type errors: a: int = "hello" is flagged
  • Undefined variables: Using a variable before defining it
  • Method existence: Calling non-existent methods on sprites
  • Full EktuPy API validation: All Stage/Sprite methods are checked

Example: Type Error Detection

# With Type Checking OFF - no error shown
a: int = "hello"

# With Type Checking ON - error shown:
# "Object of type `str` is not assignable to declared type `int`"

Toggle Dependency

The Type Checking toggle is disabled (grayed out) when LSP is off:

LSPType CheckingBehavior
OFFOFFNo checking, toggle disabled
OFFONNot allowed (toggle disabled)
ONOFFBasic syntax checking only
ONONFull type checking

Font Size

Adjust the editor font size for better readability.

  • Controls: A- (decrease), current size display, A+ (increase)
  • Range: 10px to 24px
  • Default: 14px
  • Saved to: localStorage (key: ektupy-font-size)

Settings Summary

SettingDefaultStorage KeyEffect
ThemeLightektupy-themeEditor color scheme
Enable LSPOffektupy-lspSyntax checking & autocomplete
Type Checking (ty)Offektupy-typecheckStatic type analysis
Font Size14pxektupy-font-sizeEditor text size

How the Linting System Works

Architecture Overview

┌─────────────────────────────────────────────────────┐
│                   Main Thread                        │
│  ┌──────────────┐     ┌─────────────────┐          │
│  │  CodeMirror  │────▶│  pythonLinter() │          │
│  │   Editor     │◀────│  (orchestrator)  │         │
│  └──────────────┘     └────────┬────────┘          │
│                                │                    │
│              ┌─────────────────┼─────────────────┐  │
│              │    ty enabled?  │                 │  │
│              ▼                 ▼                 │  │
│         ┌────────┐        ┌────────┐            │  │
│         │  No    │        │  Yes   │            │  │
│         └────┬───┘        └────┬───┘            │  │
└──────────────┼─────────────────┼────────────────┘
               │                 │
               ▼                 ▼
┌──────────────────────┐  ┌──────────────────────┐
│   syntax-worker      │  │    ty-worker         │
│   (Pyodide)          │  │    (ty_wasm)         │
│                      │  │                      │
│  • ast.parse()       │  │  • Full type checker │
│  • ~15MB download    │  │  • EktuPy stubs      │
│  • Basic checking    │  │  • ~13MB download    │
└──────────────────────┘  └──────────────────────┘

Data Flow

  1. User types code → CodeMirror captures changes
  2. Debounce (500ms) → Prevents excessive checking
  3. pythonLinter() → Checks which worker to use based on settings
  4. Worker receives code → Analyzes for errors
  5. Results returned → Converted to CodeMirror diagnostics
  6. UI updated → Error underlines and gutter markers displayed

Error Marker Colors

ColorMeaning
Red underlineError (syntax error, type error)
Yellow underlineWarning (unused import, deprecated usage)
Red dot in gutterLine contains an error
Yellow dot in gutterLine contains a warning

Hover over an error underline to see the full error message.