godot

Godot Dictionary - A Comprehensive Guide (GDScript 4.x)

Cover Image for Godot Dictionary - A Comprehensive Guide (GDScript 4.x)
13 min read
#godot

Dictionaries in Godot (GDScript) are the backbone of flexible data modeling: they power JSON-style configs, dynamic entity attributes, game settings, dialogue nodes, save files, localization catalogs, stat blocks, and ad‑hoc caches. While easy to start with—{"hp": 100}—they hide nuances around performance, mutation safety, iteration order, hashing, deep merging, and typed usage introduced in 4.x.

This 2025 deep-dive will teach you: creation patterns, typed dictionaries, safe lookups, defaults, merging strategies, iteration (keys / values / entries), nested structures, serialization, performance trade-offs vs Arrays, design patterns (config overlays, event payloads, component-style data packs), and common pitfalls.

1. What Is a Dictionary in Godot?

A Dictionary is an unordered (insertion-ordered for practical iteration, but not guaranteed for algorithmic reliance) associative map from keys to values. Keys can be most Variant types (numbers, strings, booleans, objects, etc.) but not those that don't support hashing well (like function references as keys). Values can be anything. Below the literal creates a mapping of string keys to mixed value types (integers and a string). This mirrors a JSON object and is the most common way you will initialize structured game state data such as stat blocks.

gdscript
var stats = {"hp": 120, "mana": 40, "name": "Knight"} print(stats["hp"]) # 120

If a key is missing and you attempt direct bracket access, Godot throws an error—so safe access patterns matter.

2. Creating Dictionaries (Core Patterns)

Below are the most common initialization approaches—literal, empty, dynamic population, and from pairs. This snippet contrasts an empty dictionary, a literal with predefined keys, incremental population (useful when keys depend on runtime logic), and constructing via the Dictionary() constructor from a 2D array of pairs (handy when transforming data from another structure).

gdscript
var empty := {} var literal := {"x": 10, "y": 42} var dynamic := {} dynamic["score"] = 0 dynamic["lives"] = 3 var from_pairs := Dictionary([["a", 1], ["b", 2]]) # Construct from array of 2-length arrays

Use the literal form whenever possible; it's clearer and marginally faster at parse time.

Typed Dictionaries (Godot 4.x)

You can now annotate expected key/value types for clarity and editor hints: Annotating both key and value types communicates intent to both the editor and collaborators. Here Dictionary[String, int] ensures all keys are strings (e.g., "strength") and all values are integers (not floats or nested objects). Attempting to assign a non-int will raise a warning, catching schema drift early.

gdscript
var attributes: Dictionary[String, int] = {"strength": 8, "agility": 11} attributes["luck"] = 5 # Type-consistent

If you try to assign a mismatched value (e.g., a string into the int typed value), the editor warns you. This improves early detection of logic bugs in growing codebases.

Mixed vs Typed Trade-off

Use typed dictionaries for stable, schema-like data (stats, configuration). Use untyped when structure evolves or you legitimately store heterogenous values.

3. Safe Access & Defaults

Direct indexing crashes if key absent: Direct access is terse but brittle; use it only when you are certain the key exists (e.g., after validation or for constants). Below, trying to access music_volume without first inserting it raises an error, which could halt gameplay logic.

gdscript
var config = {"fullscreen": true} print(config["music_volume"]) # Error if missing

Preferred patterns: Each of the following patterns reduces risk: has() for conditional presence, get() for fallback retrieval, manual get-or-add to guarantee a key will exist after the code path, and a helper wrapper when you want a centralized defaulting policy.

gdscript
# 1. has() if config.has("music_volume"): print(config["music_volume"]) # 2. get(key, default) var volume = config.get("music_volume", 0.8) # 3. get_or_add (manual pattern) if not config.has("language"): config["language"] = "en" # 4. Optional chaining style (when nested) implemented via helper func dict_get(d: Dictionary, key, fallback := null): return d.get(key, fallback)

get(key, default) is ideal for read-only fallback. For persistent mutation (ensuring the key exists after access) you must explicitly write back as shown.

4. Mutating & Removing Entries

Common mutation calls with explicit intention help avoid silent shape drift. Here we add a new key c, then remove b using erase which returns a boolean (useful if you want to log or assert that removal actually happened). Checking with has() afterward confirms state. Prefer removal over setting null if your logic distinguishes between "missing" and "present but empty".

gdscript
var d = {"a": 1, "b": 2} d["c"] = 3 # Add / replace print(d.erase("b")) # true if existed print(d.has("b")) # false

Use erase() over d["b"] = null if you truly want removal—not a sentinel value.

Clearing

gdscript
d.clear() # Empties all key/value pairs

5. Iterating Dictionaries

Iteration over a Dictionary yields keys by default. Default iteration is fine for lightweight loops, but remember each inv[key] lookup performs a hash lookup. If you reuse the value multiple times inside the loop, store it in a temporary variable to avoid repeated hashing.

gdscript
var inv = {"gold": 250, "gems": 3} for key in inv: print(key, inv[key])

Explicit key/value iteration can improve readability: Using keys() builds an Array of keys first; for huge dictionaries this allocates, but it also provides snapshot semantics if you mutate inside the loop (mutating while iterating directly over a dict can be risky if you erase keys).

gdscript
for k in inv.keys(): var v = inv[k] print(k, v)

Grab values directly when keys irrelevant: values() avoids you having to write inv[k], but like keys() it materializes an Array; use it where clarity trumps micro-performance.

gdscript
for v in inv.values(): print(v)

Or iterate over key/value tuple arrays: Mapping keys to [key, value] pairs is readable but alloc-heavy. Reserve this style for debug tooling or editor scripts, not per-frame gameplay loops.

gdscript
for pair in inv.keys().map(func(k): return [k, inv[k]]): print(pair[0], pair[1])

Be cautious: constructing pair arrays each frame allocates—avoid in hot loops.

Stable Ordering?

Although Godot often preserves insertion order in practice, do NOT design gameplay logic that relies on dictionary order. If you need a guaranteed order, extract keys, sort, then traverse.

gdscript
var ordered_keys := d.keys() ordered_keys.sort() for k in ordered_keys: process_key(k)

6. Merging Dictionaries

Merging allows layering configs (base -> platform -> user overrides). The pattern shown copies the base first to avoid mutating a canonical template, then overlays user-provided values. This is ideal for preferences screens or mod overrides where only a subset of values change.

gdscript
var base = {"volume": 1.0, "lang": "en", "fov": 90} var user = {"volume": 0.65} var merged = base.duplicate() # don't mutate base for k in user: merged[k] = user[k]

Deep Merge Utility

Nested dictionaries require recursion: deep_merge ensures nested maps (like stats or video.settings) are merged instead of replaced wholesale. Without this, a user patch supplying a single nested key would obliterate the rest of the structure. The function first deep-copies a (so original is untouched), then merges in b recursively where both sides have dictionaries.

gdscript
func deep_merge(a: Dictionary, b: Dictionary) -> Dictionary: var out = a.duplicate(true) for k in b: if out.has(k) and out[k] is Dictionary and b[k] is Dictionary: out[k] = deep_merge(out[k], b[k]) else: out[k] = b[k] return out

Use deep merges for layered settings, skill trees, or localization fallbacks.

7. Dictionaries vs Arrays

Use CasePrefer DictionaryPrefer Array
Named attributes (hp, mana)
Ordered animation frames
Dynamic runtime flags
Dense numeric grid✅ (or Packed arrays)
Sparse coordinate occupancy✅ (key = "x,y")

If you constantly iterate every element and rely on position rather than labels, an Array (or Packed) is faster and simpler.

8. Performance Considerations

ConcernImpactAdvice
Excess key churnRehash operationsReuse dictionaries; clear instead of new
Huge nested dictionariesDeep lookup costCache frequently accessed paths
Using dictionary for ordered listsIndirection + overheadSwitch to Array
Keys as large stringsHash costUse short identifiers or enums
Repeated has() + indexingDouble lookupUse get() once

Godot dictionaries are hash map based. Average operations (get/set/erase) are O(1), but pathological collisions degrade performance (rare with typical key diversity). Rehashing happens automatically when the internal bucket array grows; large growth spurts can momentarily spike frame time. Minimizing needless create/destroy cycles (especially in hot loops) reduces such spikes.

Micro Patterns

Bad (double lookup):

gdscript
if d.has("hp"): do_something(d["hp"]) # Two lookups

Better: Fetch the value once; this halves lookups and simplifies logic if you later add transform steps.

gdscript
var hp = d.get("hp", null) if hp != null: do_something(hp)

9. Nested Structures & JSON

Dictionaries + Arrays model JSON seamlessly. This enables config-driven design: Here we embed multiple nested dictionaries and arrays to represent an enemy archetype. Note that dot access (enemy_archetype.stats.hp) is invalid because dictionaries are not objects; always chain bracket lookups.

gdscript
var enemy_archetype = { "id": "orc_brute", "stats": {"hp": 300, "armor": 12, "damage": 25}, "loot": ["iron", "bone"], "ai": {"aggression": 0.8, "roam_radius": 32} } print(enemy_archetype.stats.hp) # Error! Dot access not valid print(enemy_archetype["stats"]["hp"]) # Correct

Dot notation doesn’t apply—always bracket into nested dictionaries.

Serialization

gdscript
var json_text = JSON.stringify(enemy_archetype) var parsed = JSON.parse_string(json_text)

Ensure JSON-parsed values are validated before trust (e.g., user-modifiable mod files) to avoid inconsistent runtime state. JSON.parse_string returns the Variant directly (Godot 4). Always check for unexpected types if you rely on user-authored data: missing keys, wrong types, or maliciously large structures can degrade performance.

10. Patterns & Use Cases

a. Event Payloads

Use dictionaries to pass flexible metadata through signals. Emitting a single dictionary keeps your signal signature stable even if you later add more fields (critical, source_id). Consumers ignore unfamiliar keys gracefully.

gdscript
signal item_picked(payload: Dictionary) func _on_item_pick(node): emit_signal("item_picked", {"id": node.id, "rarity": node.rarity, "value": node.value})

Receivers can inspect only the keys they care about.

b. Component-Like Data Packs

Instead of many script subclasses, attach a dictionary of tags/stats to a generic entity. This favors data-driven composition: adding meta["is_boss"] = true can alter AI or UI without subclass explosion. Later, migrate stable keys to typed Resources for stronger tooling if needed.

gdscript
var meta = {"is_flying": true, "faction": "undead", "threat": 5} if meta.get("is_flying", false): enable_altitude_logic()

c. Configuration Overlay

Layer environment-specific overrides:

gdscript
var base_cfg = {"api": "prod", "retry": 3} var dev_patch = {"api": "staging"} var dev_cfg = deep_merge(base_cfg, dev_patch)

d. Sparse Spatial Storage

gdscript
var occupancy := {} func occupy(cell: Vector2i, id): occupancy[str(cell.x) + "," + str(cell.y)] = id func is_occupied(cell: Vector2i) -> bool: return occupancy.has(str(cell.x) + "," + str(cell.y))

Avoids allocating a full grid when only a small subset contains entities. For higher performance, you can convert the "x,y" strings into a single 64-bit key: var key = (uint64(cell.x) << 32) | uint64(cell.y); this lowers hashing overhead compared to concatenated strings.

11. Defensive Programming Tips

  • Validate keys from external sources (file, network, mods).
  • Document expected keys in comments or schema resources.
  • Avoid deeply chaining: d["a"]["b"]["c"] without has() checks.
  • Normalize keys (e.g., all lowercase) when mixing human + code generation.
  • When using object references as keys, ensure their lifetime is well-scoped.

12. Debugging Dictionaries

Add temporary prints with context tags:

gdscript
print("[INV]", inv)

Pretty JSON for readability:

gdscript
print(JSON.stringify(inv, " "))

Log just keys to inspect footprint changes:

gdscript
print(inv.keys())

When debugging growth leaks, periodically log len(inv) alongside memory stats or sort keys by a naming convention to locate unintended insertions.

13. Memory & Reuse

If a dictionary shape repeats (e.g., stat blocks), consider pooling:

gdscript
var stat_pool: Array[Dictionary] = [] func acquire_stats() -> Dictionary: if stat_pool.is_empty(): return {"hp": 0, "damage": 0, "armor": 0} return stat_pool.pop_back() func release_stats(d: Dictionary): d.clear() stat_pool.append(d)

This micro-optimization only matters at scale (thousands of transient objects per frame). Do not prematurely optimize: measure allocation spikes with the profiler before introducing pools; they add complexity and potential reuse bugs if not carefully managed.

14. Common Pitfalls & Fixes

PitfallSymptomFix
Using dict for ordered dataUnpredictable order driftSwitch to Array + sort
Null vs missing key confusionUnexpected defaultsUse has() or sentinel
Over-nestingHard-to-read chainsFlatten or split modules
Large string keysSlower hashingAbbreviate or map to ints
Storing freed nodesErrors on callis_instance_valid() guard

15. Frequently Asked Questions

1. Are Dictionaries ordered in Godot?

Not guaranteed for logic. Treat them as unordered; sort keys when order matters.

2. Can I iterate keys and remove safely?

Removing while iterating can cause skipped evaluation. Collect keys to remove first:

gdscript
var to_remove: Array = [] for k in d: if should_drop(k): to_remove.append(k) for k in to_remove: d.erase(k)

3. What key types are safe?

Numbers, strings, booleans, enums, and objects (be cautious) are typical. Avoid large composite objects or volatile references unless you manage lifecycle.

4. Is get() faster than has() + []?

Yes. One lookup vs two. Use get() when you also need the value.

5. Deep vs shallow duplicate?

duplicate(true) recurses through child dictionaries/arrays. Use it when creating isolation from a template. Shallow copies share nested containers.

6. How do I provide defaults lazily?

gdscript
func ensure_key(d: Dictionary, key, fallback): if not d.has(key): d[key] = (fallback is Callable) ? fallback.call() : fallback return d[key]

7. Can I use enums as keys?

Yes; they compile to integers internally—fast and clear when modeling states.

8. Best way to clone & override a template?

Clone deep, then assign overrides:

gdscript
var unit = template.duplicate(true) unit["hp"] = 999

Deep cloning ensures nested containers (e.g., unit["stats"]) are unique; otherwise modifying nested values could inadvertently mutate the global template used for other spawns.

9. How to flatten nested dictionaries?

gdscript
func flatten_dict(d: Dictionary, prefix := "", out := {}): for k in d: var key_str = str(k) var composite = prefix == "" ? key_str : prefix + "." + key_str if d[k] is Dictionary: flatten_dict(d[k], composite, out) else: out[composite] = d[k] return out

Flattening helps export hierarchical config into a key-value store (e.g., for CSV logging or environment variable injection) while preserving structure via dotted paths.

10. How to invert a one-to-one dictionary?

gdscript
func invert(d: Dictionary) -> Dictionary: var inv := {} for k in d: inv[d[k]] = k return inv

Only safe if values are unique + hashable. If values might collide, add a guard: if inv.has(d[k]): push_warning("Duplicate inversion key") to detect logical violations early.

Conclusion

The Godot Dictionary is a powerful Swiss‑army container enabling flexible data-driven architecture. Use it for configuration layering, lightweight component metadata, event payloads, and sparse lookups. Reach for typed dictionaries when schemas stabilize, deep merge them for override modeling, and avoid overusing them for purely sequential numeric data where arrays or packed arrays outperform.

Mastering the patterns above keeps your code intentional instead of improvisational—so your future self (and collaborators) can reason about systems quickly while still iterating fast.

Follow and Support me on Medium and Patreon. Clap and Comment on Medium Posts if you find this helpful for you. Thanks for reading it!!!

Related Blogs

View All

Other Blogs

View All