Side-by-side, interactive cheatsheets for GDScript programmers
comparing GDScript to other languages. Every example runs live in your browser — no setup, no installation.
Choose your own path by reordering languages
Godot exports to the web, and when it does, your game is running next to JavaScript. The official JavaScriptBridge singleton is the seam: eval(), get_interface(), create_object() and create_callback() let a GDScript game reach the page it is embedded in. That is the concrete reason a Godot developer ends up reading JavaScript, and everything else here follows from meeting the browser's own language.
this means whatever the call site decided rather than the object you wrote it in_process — nothing calls your code each frame unless you ask, and requestAnimationFrame is what asksawait is the same word for a different machine: a Promise rather than a signal, and every async function returns one whether you meant it to or not[] is truthy, 0 is falsy, and == converts types before comparing, which is why everyone writes ===GDScript was designed to feel like Python, and it is not Python. The indentation, the colons, the for x in thing — all the same. Then the differences start, and they are the awkward kind: the same shape with different behavior rather than something obviously new. This page is mostly about the false friends, because those are what actually cost you an afternoon.
func is def, and the return arrow moves from mandatory-ish to a hint Python does not enforce at all7 / 2 is 3 in GDScript and 3.5 in Python, and nothing warns youArray becomes three things worth knowing apart: list, tuple, and a set you have never had0 are all falsy, where GDScript only gives you the numeric and null cases_process: outside a game engine, something else has to decide when your code runsjson, re, datetime, itertools and comprehensions, which is why the tooling around your game tends to end up written hereThe binding is real and people ship with it. godot-rust (gdext) registers a Rust struct as a node type the editor lists, inspects and instantiates, the same way GDExtension does for C++ — but with a compiler that refuses to build the class of bug you cannot currently see. What you trade for that is a build step, no hot reload, and a borrow checker that has opinions about the reference you were going to keep.
null. A value that might be missing is Option<T>, and the compiler will not let you use it without handling the empty caseResult<T, E> is what push_error wishes it were: a failure the caller cannot silently ignore, since ignoring it is a warning and unwrapping it is a decision you wrote downBase<Node2D> rather than extending it, and shared behavior comes from traits instead of a parent classmatch is checked for exhaustiveness, so adding a state to an enum turns every place that forgot it into a compile error rather than a silent fallthroughmut, which is the reverse of every habit you have — and the reason data races are a compile error rather than a heisenbugSwiftGodot is a real binding, and the one that reaches Apple platforms natively. Miguel de Icaza's project registers a Swift class as a Godot node type through GDExtension, and its sibling SwiftGodotKit embeds the engine inside a Swift app instead. If your game is going to iOS and you already live in Xcode, this is the path that does not ask you to leave.
null entirely — a value that might be missing is T?, and the compiler will not let you use it without unwrappingenum carries associated values, so a state and its data travel together — and switch is checked for exhaustiveness, which match is notstruct copies and class shares, so the Vector2-versus-Array distinction you already feel becomes something you declareguard let is the early-return shape that if x != null: return keeps almost expressingGodot's other official language, in the same editor, on the same scene tree. You keep every node, signal and lifecycle callback you already know — _ready becomes _Ready and $Sprite2D becomes GetNode<Sprite2D>("Sprite2D"). What changes is everything around them: types are checked before the game runs, a build step stands between you and pressing play, and the engine's reference counting now has .NET's garbage collector living beside it.
var speed or var speed := 5.0 and checks the annotated one at runtime; C# has no untyped option, and the error arrives at build time instead of on the frame that touches itbool, so if (health) does not compile where if health: quietly meant "not zero"[Signal] delegate void HealthChangedEventHandler(int amount) declares one, and connecting is HealthChanged += OnHealthChanged — a C# event, checked for arity and type at compile time rather than at emit_signal@export var speed := 200.0 becomes [Export] public float Speed = 200.0f; and still appears in the inspector, because the attribute is what the editor readsNode you QueueFree() is destroyed by the engine while its C# wrapper is collected by the GC, so a disposed node reached through a stale reference throws instead of returning nullArray that GDScript makes you write for every filter, map and groupawait get_tree().create_timer(1.0).timeout is await ToSignal(GetTree().CreateTimer(1.0), "timeout") — the same idea, with the compiler checking that the method returning it is asyncYou can script Godot in Lua, and Lua is the language most other engines script in. gilzoide's lua-gdextension registers Lua as an alternative to GDScript and C#, and separately gives you sandboxed LuaStates — which is the modding story Godot otherwise makes you build yourself. Meanwhile Love2D, Defold and Roblox are all Lua, so this is the one comparison on this anchor that is sideways rather than downward.
{1, 2, 3} is just a table whose keys happen to be 1, 2 and 3#list is the length. Every loop you have ever written has an off-by-one waiting in it0 is TRUE. Only nil and false are falsy, so if 0 then runs — the exact opposite of the check you write in GDScriptlocal, which is the reverse of every scoping habit you have and the most common source of action-at-a-distance bugs__index are how inheritance is built, and a "class" is a table you wrote the plumbing forcoroutine.yield passes values both ways, where await only resumesThe engine you already use is written in this. Godot's whole scene tree, its physics, its renderer and every built-in node are C++ — and since Godot 4, GDExtension lets you add to that layer without forking the engine. A class you register there appears in the editor's node list and is instantiated, inspected and connected exactly like a built-in one.
.gdextension file, and no reloading a class by pressing playmemnew and memdelete, Ref<T> for the reference-counted types, and a Node whose lifetime belongs to the parent you added it toArray-of-anything you reach for now, and they are checked when the library is built rather than when the frame runs_bind_methods() is the price of talking to the engine: anything GDScript, a signal, or the inspector needs to reach has to be declared there by name