INTRODUCTION
While working on a configuration engine for a multi-tenant SaaS platform, our team needed to build a highly dynamic deployment workflow. The infrastructure-as-code (IaC) and pipeline configurations were defined using large, deeply nested JSON templates. To maintain a single source of truth across multiple deployment environments—such as Development, Staging, and Production—we relied heavily on placeholder strings like {env} within these templates.
We realized quickly that managing configuration drift across environments required programmatic injection of variables. Our system would parse the JSON file into a Python dictionary, and a utility function was supposed to traverse this dictionary to replace every instance of {env} with the target environment name (e.g., dev or prod). However, what seemed like a straightforward recursive dictionary update quickly devolved into a bottleneck when we encountered nested lists, deeply embedded dictionary keys, and mixed data types.
We encountered a situation where incomplete replacements caused silent pipeline failures, routing test jobs to the wrong infrastructure. This challenge inspired this article so others can avoid the same oversights when traversing and mutating deeply nested data structures in Python.
PROBLEM CONTEXT
The core business requirement was to maintain absolute isolation between tenant environments without duplicating configuration files. The deployment engine read a master JSON blueprint, converted it into a Python dictionary, and subsequently fed it to our downstream orchestrators.
The problem surfaced inside the validation layer of our architecture. A configuration dictionary could look like this:
my_dct = {
"pipeline": "Test Pipeline",
"environment": "dev",
"test": {
"{env}_test2": {
"test3": ["{env}_test4", "{env}_test5"]
}
}
}
Our goal was to pass this dictionary and an argument like dev to a function, resulting in a fully resolved dictionary where keys like {env}_test2 and list values like {env}_test4 were correctly mapped to dev_test2 and dev_test4.
WHAT WENT WRONG
Initially, an engineer implemented a standard recursive function iterating over the dictionary items. The symptoms of failure appeared almost immediately during integration testing. The initial recursion handled dictionary values perfectly but completely ignored list structures. When the pipeline triggered, jobs requiring configurations from the nested lists failed because the raw string {env}_test4 was passed instead of the resolved environment string.
Additionally, the first implementation only replaced placeholders in dictionary values, entirely missing the dynamic dictionary keys. Attempting to modify dictionary keys during iteration threw a RuntimeError: dictionary changed size during iteration. We were patching edge case after edge case, leading to brittle, unreadable code that was hard to maintain.
HOW WE APPROACHED THE SOLUTION
We took a step back and evaluated the architecture. We realized we had two distinct approaches to solving this problem, each with its own trade-offs.
Approach 1: Pre-Parse String Manipulation
Since the initial configuration originated from a JSON file, the simplest approach was to intercept the raw string before it was serialized into a Python dictionary. Running a simple string replacement on the raw JSON content is an O(N) operation and bypasses the complexity of recursion, dictionary keys, and nested lists entirely. However, this approach is only viable if the dictionary is always read from a static file and there is no risk of the placeholder accidentally matching a structural JSON element.
Approach 2: Deep Recursive Object Traversal
For scenarios where the dictionary was already in memory—perhaps modified by preceding Python logic before the environment injection—we needed a robust recursive function. To solve the previous failures, the function needed strict type checking. It had to recursively handle dictionaries (mutating both keys and values), iterate over lists without losing their order, and correctly return primitive strings with replaced values.
We implemented both. We used the pre-parse method for static file loading, and the recursive method for in-memory dynamic configurations.
FINAL IMPLEMENTATION
Below are the implementations for both approaches. We standardized these utilities within our core library.
The Pre-Parse Method (File-Based)
import json
def load_and_resolve_json(filepath: str, env_val: str) -> dict:
with open(filepath, 'r') as file:
raw_content = file.read()
# Replace the placeholder before parsing into a dict
resolved_content = raw_content.replace("{env}", env_val)
return json.loads(resolved_content)
The Deep Recursive Traversal (In-Memory)
For dictionaries that are dynamically generated or modified in memory, we built a comprehensive recursive generator that safely handles keys, values, and iterables without mutating the original object during iteration.
def resolve_placeholders(obj, env_val: str):
# Handle Dictionaries
if isinstance(obj, dict):
return {
(k.replace("{env}", env_val) if isinstance(k, str) else k): resolve_placeholders(v, env_val)
for k, v in obj.items()
}
# Handle Lists
elif isinstance(obj, list):
return [resolve_placeholders(item, env_val) for item in obj]
# Handle Strings
elif isinstance(obj, str):
return obj.replace("{env}", env_val)
# Return other data types as-is (int, bool, float, etc.)
else:
return obj
# Usage Example
resolved_dict = resolve_placeholders(my_dct, "dev")
This recursive implementation is pure—it returns a new dictionary rather than modifying the existing one in place. This avoids the RuntimeError associated with dictionary mutation and ensures memory safety.
LESSONS FOR ENGINEERING TEAMS
- Analyze the Source Data: Before building complex recursive logic, ask where the data originates. If it comes from a text-based format like JSON, manipulating the string before parsing is often much faster and less error-prone.
- Account for all Data Structures: When traversing configurations, do not assume data consists only of nested dictionaries. Always account for lists, tuples, and primitive types to prevent silent failures.
- Mutate Keys Safely: Never mutate dictionary keys while iterating over the dictionary. Instead, generate a new dictionary (e.g., via dictionary comprehensions) to avoid runtime exceptions.
- Immutability is Safer: The recursive traversal should ideally return a new instance of the data structure. Mutating in-memory objects can lead to unintended side effects for other services sharing that memory space.
- Plan for Scalability: Complex JSON parsing bottlenecks can slow down deployment pipelines. Knowing when to optimize these utilities is a key reason enterprise tech leaders choose to hire software developer teams with deep architectural insight.
- Align Tech with Business Logic: When you hire python developers for scalable data systems, ensure they understand the business reason behind the data structure—such as our need for strict multi-tenant environment isolation.
- Standardize Core Utilities: By creating shared utilities for tasks like configuration parsing, you reduce code duplication. This is particularly crucial when you hire backend developers for enterprise modernization projects involving massive legacy codebases.
WRAP UP
Handling dynamic variables in nested dictionaries is a common but easily mismanaged challenge in automation and configuration engineering. By identifying whether to process data pre-serialization or through a memory-safe recursive function, we eliminated pipeline failures and improved system predictability. If your organization is facing complex deployment automation or architecture challenges, contact us to explore how our dedicated engineering teams can support your goals.
Social Hashtags
#Python #PythonProgramming #DevOps #JSON #SoftwareEngineering #BackendDevelopment #SaaS #CloudComputing #Automation #TechBlog #InfrastructureAsCode #IaC #Programming #Developer #PythonDeveloper #ConfigurationManagement #DataEngineering #SoftwareDevelopment #EnterpriseSoftware #Coding
Frequently Asked Questions
Python has a default recursion limit (usually 1000). While most JSON configurations rarely reach this depth, exceeding it will throw a RecursionError. For exceptionally deep structures, an iterative approach using a stack or queue is recommended over pure recursion.
Yes, provided the placeholder format is highly unique (like {env} or __ENV__). If your placeholder is too generic, you risk accidentally replacing parts of the JSON structure itself. Always use distinctive placeholder wrappers.
The recursive approach we implemented returns a new dictionary. For extremely large configurations (multi-megabyte JSONs), this temporarily doubles the memory footprint for that object. In environments with strict memory constraints, an iterative, in-place update strategy might be necessary.
The provided recursive function targets dictionaries and lists natively, which covers 99% of JSON-parsed structures (since JSON does not support tuples or sets). If you are processing native Python objects that contain tuples, you would add an elif isinstance(obj, tuple): return tuple(resolve_placeholders(item, env_val) for item in obj) block.
Success Stories That Inspire
See how our team takes complex business challenges and turns them into powerful, scalable digital solutions. From custom software and web applications to automation, integrations, and cloud-ready systems, each project reflects our commitment to innovation, performance, and long-term value.

California-based SMB Hired Dedicated Developers to Build a Photography SaaS Platform

Swedish Agency Built a Laravel-Based Staffing System by Hiring a Dedicated Remote Team

















