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

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.
gdscriptvar 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).
gdscriptvar 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.
gdscriptvar 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.
gdscriptvar 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".
gdscriptvar 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
gdscriptd.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.
gdscriptvar 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).
gdscriptfor 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.
gdscriptfor 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.
gdscriptfor 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.
gdscriptvar 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.
gdscriptvar 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.
gdscriptfunc 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 Case | Prefer Dictionary | Prefer 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
Concern | Impact | Advice |
---|---|---|
Excess key churn | Rehash operations | Reuse dictionaries; clear instead of new |
Huge nested dictionaries | Deep lookup cost | Cache frequently accessed paths |
Using dictionary for ordered lists | Indirection + overhead | Switch to Array |
Keys as large strings | Hash cost | Use short identifiers or enums |
Repeated has() + indexing | Double lookup | Use 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):
gdscriptif 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.
gdscriptvar 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.
gdscriptvar 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
gdscriptvar 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.
gdscriptsignal 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.
gdscriptvar meta = {"is_flying": true, "faction": "undead", "threat": 5} if meta.get("is_flying", false): enable_altitude_logic()
c. Configuration Overlay
Layer environment-specific overrides:
gdscriptvar base_cfg = {"api": "prod", "retry": 3} var dev_patch = {"api": "staging"} var dev_cfg = deep_merge(base_cfg, dev_patch)
d. Sparse Spatial Storage
gdscriptvar 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"]
withouthas()
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:
gdscriptprint("[INV]", inv)
Pretty JSON for readability:
gdscriptprint(JSON.stringify(inv, " "))
Log just keys to inspect footprint changes:
gdscriptprint(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:
gdscriptvar 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
Pitfall | Symptom | Fix |
---|---|---|
Using dict for ordered data | Unpredictable order drift | Switch to Array + sort |
Null vs missing key confusion | Unexpected defaults | Use has() or sentinel |
Over-nesting | Hard-to-read chains | Flatten or split modules |
Large string keys | Slower hashing | Abbreviate or map to ints |
Storing freed nodes | Errors on call | is_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:
gdscriptvar 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?
gdscriptfunc 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:
gdscriptvar 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?
gdscriptfunc 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?
gdscriptfunc 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!!!