GDScript Tutorial for People Who Know Some Python
A GDScript tutorial for Python programmers. A translation table with the documented traps, one complete Godot 4.7 script using every feature, typing habits.

If you know some Python, this GDScript tutorial should take an afternoon rather than a course. GDScript is indentation based, dynamically typed until you add hints, and close enough to Python that the Godot FAQ says "if you've ever written anything in a language like Python before, then you'll feel right at home." What will bite you is a short list. Dividing two ints gives an int. The % operator refuses floats. Lambdas copy variables instead of borrowing them. A failed cast returns null without a word. There are no list comprehensions, no tuples, no exceptions and no imports. The rest of the difference is Godot itself, which Python has no equivalent for, and that part is the point.
Below is a translation table with the traps marked, then one complete Godot 4.7 script that uses every feature in the table, then the typing habits that make the editor catch your mistakes before you run anything. Every syntax claim was checked against the 4.7 GDScript reference today.
Where the Beginner Resources Stop
Search this phrase and most of page one points at the same thing, an app called Learn GDScript From Zero. The docs recommend it as "a complete beginner course with interactive practices you can do right in your browser," and it is good at that job. Its own README is honest about the limit, though. It is "for absolute beginners" and "not a complete programming course meant to go in-depth into programming." It covers variables, functions, arrays, dictionaries and the processing loop.
If you've written a for loop before, you'll be bored by the third lesson, and the app will not tell you the things that actually trip a Python programmer, because a Python programmer is not who it is for. My problem with it is narrower than that, though. It never gets to signals, and signals are the thing you need to understand before any Godot script makes sense.
The other written resource that ranks is a nine-page concept list with no engine version on it. Fine for "what is a variable." Useless for "why did my division come out wrong."
So this page assumes you can already program and spends its words on the differences.
Python to GDScript, With the Traps Marked
Each row's GDScript column comes from the reference or the static typing guide. The trap column is where I'd expect a Python habit to produce a wrong answer or a silent failure.
| Python | GDScript | Trap or note |
|---|---|---|
x = 5 |
var x = 5, var x: int = 5, or var x := 5 |
:= infers the type; no hint means Variant |
5 / 2 is 2.5 |
5 / 2 is 2 |
two ints divide as ints; write 5 / 2.0 or float(x) / y |
-7 % 3 is 2 |
-7 % 3 is -1 |
% is ints only and truncates; fmod() for floats, posmod() for the math answer |
2 ** 3 ** 2 is 512 |
2 ** 3 ** 2 is 64 |
every binary operator is left associative, including power |
None |
null |
same idea |
and, or, not |
same | &&, ` |
a if c else b |
same | right associative, chains the same way |
[1, 2, 3] |
[1, 2, 3], or Array[int] typed |
PackedInt32Array and friends for tight storage |
{"k": 1} |
same, plus {k = 1} Lua style |
typed as Dictionary[String, int]; nested typed collections not supported |
for i in range(3) |
same | for i in 3 also works |
for k, v in d.items() |
for k in d: then d[k] |
no tuple unpacking |
[x * 2 for x in xs] |
xs.map(func(x): return x * 2) |
no comprehensions; filter, reduce, any, all exist |
lambda x: x * 2 |
func(x): return x * 2 |
return is required; locals are captured by value once |
class Foo(Bar): |
a file is a class; class_name Foo then extends Bar |
one class per file, named classes are global |
import |
none; preload(), load(), or a class_name |
there is no module system |
try / except |
none | assert() in debug builds, push_error(), return values |
(1, 2) |
none | Vector2i, an Array, or a small class |
"%s" % x |
same | .format() also exists; f-strings do not |
self.x required |
self optional |
self.x is allowed and sometimes clearer |
is compares identity |
is checks type |
x is Node; == compares values |
| four spaces | tabs | the style guide says "Use Tabs instead of spaces for indentation" |
Two rows deserve a sentence each because they fail quietly.
The division row is the one I'd put money on. Python 3 made / always fractional, and years of that habit mean damage / 2 on an int reads as obviously safe. In GDScript the reference is explicit that "if both operands of the / operator are int, then integer division is performed instead of fractional." It goes on to say the operators "have the same behavior as C++, which may be unexpected for users coming from Python." Your halved damage is floored, and nothing warns you.
The lambda row is the other. The reference says "local variables are captured by value once, when the lambda is created," and "a lambda cannot reassign an outer local variable." A Python closure sees later changes to the outer variable. A GDScript lambda has a snapshot. If you connect a lambda to a signal and expect it to read a counter you keep updating, make the counter a member variable, because the lambda's copy will never move.
On tabs, the code blocks on this page use spaces because that is what a web page renders reliably. The editor inserts tabs when you press Tab. Indentation is significant, so if you paste from here, check that the pasted block did not end up mixing the two. My habit is to retype the indentation on anything I paste, which is slower and has never once cost me a debugging session.
One Script That Uses All of It
Create an empty scene with a Node root, attach this script as inventory.gd, and run the scene. Everything it prints comes from the features in the table plus the three Godot-only ideas covered after it. The numbers are example values, nothing about eight slots or a stack of 16 is a recommendation, and I kept them small so the output fits in one glance.
class_name Inventory
extends Node
signal item_added(item: StringName, count: int)
signal capacity_reached
enum Rarity {COMMON, RARE, EPIC}
const MAX_SLOTS: int = 8
@export var starting_gold: int = 25
@export_range(1, 64) var stack_limit: int = 16
var gold: int = 0:
set(value):
gold = maxi(value, 0)
var slots: Dictionary[StringName, int] = {}
func _ready() -> void:
item_added.connect(_on_item_added)
capacity_reached.connect(func(): print("no free slot"))
gold = starting_gold
gold -= 40
add_item(&"arrow", 12)
add_item(&"arrow", 10)
add_item(&"potion")
print(describe())
print(rarity_label(Rarity.EPIC))
await get_tree().create_timer(1.0).timeout
print("one second later, gold is %d" % gold)
func add_item(item: StringName, count: int = 1) -> bool:
if not slots.has(item) and slots.size() >= MAX_SLOTS:
capacity_reached.emit()
return false
var current: int = slots.get(item, 0)
slots[item] = mini(current + count, stack_limit)
item_added.emit(item, slots[item])
return true
func rarity_label(rarity: Rarity) -> String:
var label := "common"
match rarity:
Rarity.RARE:
label = "rare"
Rarity.EPIC:
label = "epic"
return label
func describe() -> String:
var parts: PackedStringArray = []
for item: StringName in slots:
parts.append("%s x%d" % [item, slots[item]])
return ", ".join(parts)
func _on_item_added(item: StringName, count: int) -> void:
print("added %s, stack is now %d" % [item, count])
Run it and the output panel should read, in order, "added arrow, stack is now 12", "added arrow, stack is now 16", "added potion, stack is now 1", "arrow x16, potion x1", "epic", and after a pause, "one second later, gold is 0".
That last line is the setter doing its job. gold was set to 25 and then had 40 subtracted, and the setter clamped it at zero. If you expected minus 15, read the next section.
The Setter Guards the Value
Python does this with a @property and a second method. GDScript puts the set(value): block directly under the variable. The reference notes that "set and get methods are always called ... even when accessed inside the same class," which is a change from Godot 3 and the reason you can trust the clamp from anywhere in the script. It also answers the recursion worry. Assigning gold inside its own setter "will directly access the underlying member, so it won't generate infinite recursion."
One restriction the docs state and I'd have guessed wrong. "You cannot specify type hints for inline setters and getters." The argument takes the variable's type automatically.
Signals Are the Part Python Does Not Have
signal item_added(item: StringName, count: int) declares a message this node can send. item_added.emit(item, slots[item]) sends it. item_added.connect(_on_item_added) in _ready() subscribes a method, and the second connection subscribes a lambda. The reference describes signals as "a tool to emit messages from an object that other objects can react to," and the argument list you declare is what shows up in the editor's Signals dock.
The pattern this enables is the one that keeps a game from turning into a knot. The inventory does not know a HUD exists. The HUD connects to item_added and updates itself. Delete the HUD and the inventory keeps working. The Godot UI tutorial on this site is where you'll see signals wired to buttons, which is where most people meet them first.
Naming follows the style guide, which says to "use the past tense to name signals." item_added, not add_item. The verb form is the method that causes the signal.
Match, Enum and Why Not a Dictionary
enum Rarity {COMMON, RARE, EPIC} is, per the reference, "basically a shorthand for constants," with the values 0, 1 and 2. Typing a parameter as Rarity means the editor completes the options and flags a stray integer.
match looks like Python's, and the patterns are similar. Literals, a _ wildcard, var name bindings, arrays, dictionaries, comma-separated alternatives, and when guards. The difference is age. Python's match arrived in 3.10 and a lot of Python code still avoids it. In GDScript it is the normal way to branch on a state enum, and I'd use it over an if chain any time there are more than two cases.
A dictionary lookup would also work here. I keep match when the branches might grow logic and a dictionary when they are pure data, and I would not argue hard with someone who drew the line elsewhere.
Await Turns a Function Into a Coroutine
await get_tree().create_timer(1.0).timeout pauses _ready() for one second and then resumes it. The reference says await "can be used to create coroutines which wait until a signal is emitted before continuing execution," and create_timer() returns a one-shot timer whose timeout signal fires after the given seconds and then frees itself.
The trap is the return value. Once a function contains an await, it becomes a coroutine, and a caller that wants its return value must also await it. Calling it plainly and reading the result is an error. Calling it plainly and ignoring the result is fine and does not block. Python's async and await split the world into two kinds of function the same way, so if you've used asyncio, the rule is familiar. If you haven't, the rule I'd write on a sticky note is still short. Awaiting anything that is not a signal or coroutine returns immediately and changes nothing.
Type Everything You Can
Godot calls the language "gradually typed," and the gradual part is a choice you make per line. My advice is to make it once, for the whole project.
The static typing guide gives three reasons, and I'll take them in the order that mattered to me. "Static types can help you avoid bugs." Typed lines turn the line number green in the editor, which the guide calls a safe line, so an untyped call stands out at a glance. And "typed GDScript improves performance by using optimized opcodes when operand/argument types are known at compile time."
The habit is :=. var damage := 10.5 infers float from the literal. Where inference is ambiguous, write the type. var current: int = slots.get(item, 0) in the script above exists because Dictionary.get() returns Variant and I wanted the int on record.
Then switch on the warning. Under Project Settings, Debug, GDScript, with Advanced Settings enabled, the guide says to enable UNTYPED_DECLARATION if you want consistent typing. From then on every var x = 5 without a hint gets flagged, and the flag is annoying in exactly the useful way.
The cast operator is the one to be careful with. The guide is blunt that "the as keyword silently casts the variable to null in case of a type mismatch at runtime, without an error/warning." So $Sprite as AnimatedSprite2D on a node that is really a Sprite2D hands you null and lets you crash three lines later. If the type could be wrong, check it with is first. If it can't be wrong, declare the variable with the type instead of casting.
The Three Ideas With No Python Equivalent
The table covered syntax. These are the parts of GDScript that exist because of the engine, and none of the beginner resources explain why they feel odd to someone from Python.
A file is a class, and the scene instantiates it. There is no if __name__ == "__main__" and no entry point you call. You attach the script to a node in a scene, the engine builds the scene tree, and your class is constructed as part of it. extends Node at the top is not optional in practice, because a script has to inherit from something the tree can hold. $ChildName is shorthand for get_node("ChildName") and %UniqueName reaches a node marked unique in its owner. The @onready annotation "defers initialization of a member variable until _ready() is called," which is when those children exist. The Godot 2D tutorial on this site builds a whole game on that pattern, and it is the natural next page after this one.
The engine calls you. _ready(), _process(delta) and _physics_process(delta) are methods you fill in and never call yourself. The underscore prefix is the style guide's marker for virtual methods and private members. If you've written a Flask route or a pytest fixture, you know the shape. The framework owns the loop.
Memory is reference counted, not garbage collected. The reference states that Godot uses "reference counting to free certain instances that are no longer used, instead of a garbage collector." Anything inheriting RefCounted, which includes every Resource, frees itself. A Node does not. It stays "until it is deleted with free() (or queue_free() for Nodes)," and deleting a node deletes its children. Python programmers rarely think about lifetime. In GDScript, the node you forgot to free is still in the tree, still processing, and still costing you a frame budget, and is_instance_valid() is how you check whether a reference you kept still points at something alive.
Is GDScript Worth Learning Instead of C#
Godot's officially supported languages are GDScript, C# and C++. The FAQ's own answer on speed is that "in most games, the scripting language itself is not the cause of performance problems," and that "the performance of C# and GDScript is within the same order of magnitude, and C++ is faster than both."
I'd start in GDScript even if you already know C#, for a reason the FAQ also gives. The docs recommend it for newcomers because "we designed it to be simpler than all-purpose languages like C#," and the practical version of that is that every example in the class reference has a GDScript tab, and every tutorial on this site is written in it. You can add C# later for a hot path. The Godot vs Unity comparison here covers the case where you're choosing an engine, not just a language.
On why the language exists at all, the FAQ records that the engine started on Lua, that "after some experiments with Python, that also proved difficult to embed," and lists threading, class extension, C++ binding cost, missing vector types and garbage collector stalls as the reasons for a custom language. So the Python resemblance is deliberate and the independence is too. The reference says it plainly. "GDScript is entirely independent from Python and is not based on it."
What to Do With the Next Hour
Copy the Inventory script, run it, then break it. Change starting_gold in the inspector and confirm the export overrides the code. Assign a string to gold and read the error before you even run it. Add a ninth item name and see capacity_reached fire. Replace mini() with a raw addition and see the stack pass 16.
Then open the first Godot game guide if the editor itself is still new to you, or go straight to the 2D tutorial and write the player script without looking at its code first. You know enough now. The language was never the hard part of Godot. The tree is, and you learn that by building.
If a friend has never programmed, send them the app the docs recommend. It is free, it runs in a browser, and it is the right first step for them. It was just never the right one for you.
Related Articles

Godot Engine 2D Tutorial: A Complete Small Game
A Godot engine 2D tutorial building one complete small game in Godot 4.7 with CharacterBody2D movement, coins, a countdown, best score, restart and web export.

How to Get Into Game Development Without a Degree
Get into game development without a degree using a $0 tool stack with licenses checked, a 12-week plan, a four-rung portfolio ladder and the failures to expect.

No Coding Game Engine: One Game Spec, Five Engines
Pick a no coding game engine by checking one small game spec against GDevelop, Construct 3, GameMaker, Unreal and Godot, using official docs and free-tier caps.