Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions tutorials/scripting/gdscript/gdscript_nullable_types.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
.. _doc_gdscript_nullable_types:

Nullable static types
=====================

This guide builds on :ref:`doc_gdscript_static_typing`. In statically-typed
GDScript, a value is **non-nullable** by default: the type system flags ``null``
as an illegal value for it. Writing a ``?`` after a type hint makes the value
**nullable**, opting it back into holding ``null``::

var health: int? # May be an int or null; starts as null.
var player_name: String? = null

This is purely a static typing feature. It does not change how values are stored
at runtime — ``null`` is still ``Nil``. It changes what the analyzer allows and
what the virtual machine enforces.
Comment on lines +6 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 4 'Only types that inherit from Object|Variant types.*null' tutorials/scripting/gdscript/gdscript_basics.rst
rg -n -C 4 'int\?|String\?|Array\?' tutorials/scripting/gdscript/gdscript_nullable_types.rst

Repository: Redot-Engine/redot-docs

Length of output: 5549


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- gdscript_basics.rst ---'
sed -n '680,705p' tutorials/scripting/gdscript/gdscript_basics.rst

printf '%s\n' '--- gdscript_nullable_types.rst ---'
sed -n '1,75p' tutorials/scripting/gdscript/gdscript_nullable_types.rst
sed -n '130,180p' tutorials/scripting/gdscript/gdscript_nullable_types.rst

printf '%s\n' '--- related nullability wording ---'
rg -n -C 3 'Only types that inherit from Object|Variant types|nullable|non-nullable|Array\?|Dictionary\?' tutorials --glob '*.rst'

Repository: Redot-Engine/redot-docs

Length of output: 23497


Update the nullability description in gdscript_basics.rst.

The statements at lines 695–698 conflict with the nullable static types documented in gdscript_nullable_types.rst. Explain that Variant already includes null, while statically typed built-in types require the ? suffix to accept it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tutorials/scripting/gdscript/gdscript_nullable_types.rst` around lines 6 -
16, Update the nullability explanation in gdscript_basics.rst to state that
Variant already permits null, while statically typed built-in types are
non-nullable by default and require the ? suffix to accept null. Align the
wording with the behavior documented in gdscript_nullable_types.rst.


.. note::

How strictly "non-nullable" is enforced depends on the type. Built-in value
types (``int``, ``String``, ``Array``, and so on) are checked both when used
directly and at runtime. Enums are checked on transfer and at runtime. Object
references (``Node``, ``RefCounted``, script classes) keep GDScript's existing
behavior and may hold ``null`` regardless of the ``?``. See
Comment on lines +20 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 3 'Built-in value types|Array\?|Dictionary\?' tutorials/scripting/gdscript/gdscript_nullable_types.rst
rg -n -C 3 'reference type|value type|Array|Dictionary' --glob '*.rst' --glob '*.cpp' --glob '*.h' .

Repository: Redot-Engine/redot-docs

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- nullable-types excerpt ---'
sed -n '1,175p' tutorials/scripting/gdscript/gdscript_nullable_types.rst
printf '%s\n' '--- basics terminology ---'
rg -n -C 4 'passed by reference|passed by value|value type|reference type|Array and Dictionary|Array.*Dictionary' tutorials/scripting/gdscript/gdscript_basics.rst
printf '%s\n' '--- focused nullable references ---'
rg -n -C 5 'Built-in value types|non-object|Array|Dictionary' tutorials/scripting/gdscript/gdscript_nullable_types.rst

Repository: Redot-Engine/redot-docs

Length of output: 12956


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- remaining nullable-types excerpt ---'
sed -n '175,225p' tutorials/scripting/gdscript/gdscript_nullable_types.rst
printf '%s\n' '--- deterministic terminology check ---'
python3 - <<'PY'
from pathlib import Path
basics = Path("tutorials/scripting/gdscript/gdscript_basics.rst").read_text()
nullable = Path("tutorials/scripting/gdscript/gdscript_nullable_types.rst").read_text()
needle = "Built-in types are stack-allocated. They are passed as values."
start = basics.index(needle)
end = basics.index("Basic built-in types", start)
print(basics[start:end].strip())
for term in ("Array", "Dictionary"):
    print(f"{term}: nullable note={term in nullable[nullable.index('How strictly'):nullable.index('Syntax')]}; "
          f"direct-use section={term + '?' in nullable[nullable.index('The UNSAFE_NULLABLE_ACCESS'):nullable.index('Runtime enforcement')]}")
PY

Repository: Redot-Engine/redot-docs

Length of output: 2591


Replace “built-in value types” with “non-object built-in types” in both sections. Array and Dictionary are passed by reference, unlike int and String. The current wording incorrectly classifies Array as a value type, including in the UNSAFE_NULLABLE_ACCESS description.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tutorials/scripting/gdscript/gdscript_nullable_types.rst` around lines 20 -
24, Update the nullable-types documentation wording in both referenced sections,
including the UNSAFE_NULLABLE_ACCESS description, replacing “built-in value
types” with “non-object built-in types.” Ensure the examples distinguish
reference-backed Array and Dictionary from value types such as int and String.

`Notes on objects and enums`_ below for the details.

Syntax
------

The ``?`` suffix is allowed anywhere a type hint is allowed: member variables,
local variables, function parameters, and return types::

var member: int?

func f(v: int?) -> int?:
var local: Vector2? = null
return null

An uninitialized nullable variable defaults to ``null``, whereas a non-nullable
``int`` would default to ``0``::

var x: int?
print(x) # <null>

Compatible types
----------------

The ``?`` suffix works on nearly every type you can write as a hint:

+-----------------------+--------------------------------------------------------------------------------------------+
| Category | Examples |
+=======================+============================================================================================+
| Built-in value types | ``int?``, ``float?``, ``bool?``, ``String?``, ``StringName?``, ``Vector2?``, ``Color?``, … |
+-----------------------+--------------------------------------------------------------------------------------------+
| Enums | ``State?`` (script enums), ``Vector2.Axis?``, ``Variant.Type?`` (native and nested enums) |
+-----------------------+--------------------------------------------------------------------------------------------+
| Objects | ``Node?``, ``RefCounted?``, and script classes such as ``MyClass?`` |
+-----------------------+--------------------------------------------------------------------------------------------+
| Collections | ``Array?``, ``Dictionary?`` |
+-----------------------+--------------------------------------------------------------------------------------------+
| Typed collections | ``Array[int]?``, ``Dictionary[String, int]?`` — the *collection* is nullable |
+-----------------------+--------------------------------------------------------------------------------------------+

The following forms are rejected because they are redundant or ambiguous:

- ``Variant?`` — :ref:`Variant <class_Variant>` already includes ``null``, so the
suffix is redundant and raises an error.
- ``void?`` — a function cannot return "maybe nothing"; this is a parser error.
- **Nullable element types**, such as ``Array[int?]`` or
``Dictionary[String, int?]`` — only the container itself can be made nullable,
not its elements. Use ``Array[int]?`` (a nullable array of ints) instead.

Null-narrowing
--------------

After you check a nullable value against ``null``, the analyzer *narrows* it to
its non-nullable type for the rest of the safe region, so you can use it without
a warning::

func guard(v: int?) -> int:
if v == null:
return -1
return v # Here v is known to be non-null (int).

func branch(v: int?) -> int:
if v != null:
return v + 1 # Narrowed inside the block.
return 0

Narrowing is recognized for:

- **Early-return / early-exit guards:** ``if v == null: return``, then use ``v``
afterwards.
- **Positive blocks:** the body of ``if v != null:``.
- **else branches:** the ``else`` of an ``if v == null:`` guard.
- **while conditions:** the body of ``while v != null:``.
- **Boolean and:** ``v != null and v > 5`` — the right-hand side sees ``v`` as
non-null.
- **or guards:** ``if a == null or b == null: return`` narrows both ``a`` and
``b`` after the guard.
- **Reassignment with a non-null value** keeps the value narrowed::

if v != null:
v = v + 1 # Still non-null.
return v

Narrowing is intentionally conservative. In the following cases the value stays
nullable and still produces the ``UNSAFE_NULLABLE_ACCESS`` warning:

- **Reassignment from a nullable source** re-widens the value; check it again::

if v != null:
v = get_nullable() # v is nullable again.
return v + 1 # Warns.

- **break / continue guards** are not treated as narrowing::

while true:
if v == null:
break
return v + 1 # Warns — break guard is not narrowed.

- **Non-definite guards** (a guard that does not unconditionally exit)::

if x == null:
if cond:
return -1
return x + 1 # Warns — the guard might fall through.

- Combinations that do not actually prove non-null, such as
``a == null and b == null`` or ``a != null or b != null``, do not narrow ``a``
in the branch.

The UNSAFE_NULLABLE_ACCESS warning
----------------------------------

The analyzer emits ``UNSAFE_NULLABLE_ACCESS`` in two situations.

First, when a nullable **built-in value** (``int?``, ``String?``, ``Vector2?``,
``Array?``, and so on) is used directly — in an operator, a subscript or property
access, or a ``for`` loop — without first narrowing it::

var v: Vector2? = Vector2(3, 4)
print(v.x) # Warns: the value of type "Vector2?" may be null.

Second, when a nullable value of **any** type is transferred to a non-nullable
target: assigned to it, returned as it, or passed as an argument::

var a: int? = 5
var b: int = a # Warns: assigning nullable to non-nullable.
takes_int(a) # Warns: nullable argument to non-nullable parameter.

The direct-use check only applies to built-in value types. Nullable enums and
nullable object references are covered by the transfer check but not the
direct-use one — see `Notes on objects and enums`_ below.

The severity is configured by the
:ref:`debug/gdscript/warnings/unsafe_nullable_access<class_ProjectSettings_property_debug/gdscript/warnings/unsafe_nullable_access>`
project setting (default: warn). Set it to ``error`` to make unsafe nullable
access a hard compile error, or ``ignore`` to silence it. As with any GDScript
warning, you can suppress a single site with
``@warning_ignore("unsafe_nullable_access")``. See :ref:`doc_gdscript_warning_system`
for details on the warning system.

Runtime enforcement
-------------------

The warning is a static hint; for built-in value types and enums the runtime
independently rejects ``null`` reaching a non-nullable slot. Even if you ignore
the warning, the following fail at runtime::

func bad(v: int?) -> int:
return v # If v is null: runtime error.

func use() -> void:
var value: int? = get_nullable()
print(value + 1) # If null: "Invalid operands 'Nil' and 'int'".

Typical runtime messages are:

- ``Trying to return value of type "Nil" from a function whose return type is "int".``
- ``Invalid operands 'Nil' and 'int' in operator '+'.``

Typed containers keep their own element checks. Returning a wrongly-typed array
from ``-> Array[int]?`` still errors with ``Trying to return an array of type
"Array" where expected return type is "Array[int]"``.

Notes on objects and enums
--------------------------

Object references (``Node``, ``RefCounted``, script classes) keep GDScript's
existing nullability. Unlike built-in value types, an object type accepts ``null``
at runtime whether or not it is marked with ``?``, so a null object reaching a
non-nullable object type is **not** a runtime error.

What ``?`` adds for objects is static tracking. ``Node?`` participates in
null-narrowing and raises ``UNSAFE_NULLABLE_ACCESS`` when the value is transferred
to a non-nullable target (assigned, returned, or passed as an argument). Accessing
a member or calling a method directly on a nullable object does *not* currently
raise the warning; only built-in value types are checked for direct use.
Comment on lines +188 to +200

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 4 'null instance|Attempt to call|UNSAFE_NULLABLE_ACCESS|Node\?' --glob '*.rst' --glob '*.cpp' --glob '*.h' .

Repository: Redot-Engine/redot-docs

Length of output: 7209


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- nullable-types.rst ---'
sed -n '130,215p' tutorials/scripting/gdscript/gdscript_nullable_types.rst
printf '%s\n' '--- null-instance documentation ---'
sed -n '245,265p' getting_started/first_2d_game/03.coding_the_player.rst
sed -n '2125,2135p' classes/class_node.rst
printf '%s\n' '--- repository scope ---'
git ls-files | rg '(^|/)(gdscript_nullable_types|gdscript|script|variant|object|node)' | head -120

Repository: Redot-Engine/redot-docs

Length of output: 12776


🌐 Web query:

Godot GDScript nullable object Node? null instance method call runtime official documentation

💡 Result:

In GDScript, calling a method on a null object or a previously freed instance results in a runtime error [1][2]. Because GDScript is dynamically typed and objects are nullable, attempting to access properties or call functions on a null reference will cause the engine to halt execution with an error such as "Invalid call. Nonexistent function" or "Cannot call method on a null value" [1][2]. To safely handle potentially null objects, you should implement the following patterns: 1. Use the Safe Call Operator (?.) The null-safe call operator (introduced in Godot 4) allows you to call a method or access a property only if the instance is not null [2]. If the instance is null, the expression short-circuits and returns null instead of throwing an error [3][2]. Example: node?.my_method() 2. Use is_instance_valid Before calling methods on an object that may have been freed or is potentially null, use the global function is_instance_valid [2]. This is the standard practice for checking if an object still exists in memory [2]. Example: if is_instance_valid(node): node.my_method 3. Avoid common pitfalls - @onready: Ensure node references are initialized correctly, often using the @onready annotation to avoid accessing nodes before they are added to the scene tree [1][4]. - Lifecycle management: Be cautious when using queue_free or free, as references to those objects immediately become invalid [4][2]. - Pathing: Verify that get_node paths are correct; if a path is invalid, it returns null, which will crash your code if accessed directly [1][5]. While some languages or proposed features may allow more graceful handling of nulls, GDScript requires these explicit checks to maintain safety at runtime [6][3].

Citations:


🌐 Web query:

site:docs.godotengine.org/en/stable GDScript null instance method call null object is_instance_valid

💡 Result:

In GDScript, calling a method on a null instance will trigger a runtime error [1]. It is essential to ensure an object is valid before attempting to access its members or methods [1]. When working with Godot Objects (which include Nodes), a variable may contain a reference to an object that has been freed from memory, even if the variable itself is not null [1]. Therefore, checking against null (e.g., if my_node!= null) is insufficient for verifying if an object still exists [1]. To safely check if an object instance is still valid, use the built-in function is_instance_valid [1][2]: if is_instance_valid(my_node): my_node.my_method Additionally, if you are unsure whether an object has a specific method, you can use the has_method function to prevent runtime errors [3][1]: if is_instance_valid(my_node) and my_node.has_method("my_method"): my_node.my_method For objects that inherit from RefCounted, the memory is managed automatically and the reference will typically become null when the object is no longer in use, but is_instance_valid remains a robust way to verify any Object-derived instance [3][1].

Citations:


Document runtime failures for direct nullable-object access.

Although Node? member and method access does not emit UNSAFE_NULLABLE_ACCESS, access can still fail at runtime when the reference is null. State this explicitly and add a narrowed example:

if node != null:
    node.some_method()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tutorials/scripting/gdscript/gdscript_nullable_types.rst` around lines 188 -
200, Update the “Notes on objects and enums” section to explicitly state that
direct member access or method calls on a null nullable-object reference can
still fail at runtime, despite no warning being emitted. Add a narrowed GDScript
example using `if node != null:` before calling `node.some_method()`.


Nullable enums behave the same way for direct use: an operator or subscript on a
nullable enum is not flagged, but transferring it to a non-nullable enum warns,
and — because enums are backed by integers — reaching a non-nullable enum with
``null`` fails at runtime.

Freed objects keep GDScript's existing semantics. After ``node.free()``, comparing
``node == null`` returns ``true``, while ``is_instance_valid(node)`` returns
``false`` — the reference still points at the freed instance but compares equal to
``null``.
Comment on lines +207 to +210

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 4 'is_instance_valid|freed instance|previously freed|== null' --glob '*.rst' --glob '*.cpp' --glob '*.h' .

Repository: Redot-Engine/redot-docs

Length of output: 25423


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- nullable tutorial context ---'
sed -n '180,215p' tutorials/scripting/gdscript/gdscript_nullable_types.rst

printf '%s\n' '--- Object validity documentation ---'
sed -n '40,55p;1038,1047p' classes/class_object.rst

printf '%s\n' '--- is_instance_valid documentation ---'
sed -n '6128,6136p' classes/class_@globalscope.rst

printf '%s\n' '--- repository references to freed-object equality ---'
rg -n -C 3 'freed objects|freed instance|become invalid|equal to ``null``|compare.*null|is_instance_valid' \
  --glob '*.rst' --glob '*.md' --glob '*.cpp' --glob '*.h' .

Repository: Redot-Engine/redot-docs

Length of output: 19084


Use is_instance_valid() as the freed-object check.

The tutorial contradicts the Object documentation. A freed object reference can remain non-null, so node == null must not be presented as a general freed-object check. State that is_instance_valid(node) is required to determine whether the object still exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tutorials/scripting/gdscript/gdscript_nullable_types.rst` around lines 207 -
210, Update the freed-object explanation near the nullable GDScript example to
state that node == null is not a general freed-object check because a freed
reference may remain non-null. Identify is_instance_valid(node) as the required
check for determining whether the object still exists, and remove the
contradictory claim that node == null reliably detects freed objects.

1 change: 1 addition & 0 deletions tutorials/scripting/gdscript/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ GDScript
gdscript_documentation_comments
gdscript_styleguide
static_typing
gdscript_nullable_types
warning_system
gdscript_format_string

Expand Down
Loading