How to Convert String to Nested Dictionary in Python
Parsing nested dictionary strings requires identifying the format first: Python literal syntax uses single quotes and Python keywords (True, None), while JSON uses double quotes and its own keywords (true, null). Using the wrong parser causes errors.
Parse Python Literal Strings with ast.literal_eval()
When the string contains single quotes or Python-specific values like True, False, or None, use the ast.literal_eval() function.
import ast
python_string = "{'user': {'id': 1, 'active': True, 'data': None}}"
data = ast.literal_eval(python_string)
print(data)
# Output: {'user': {'id': 1, 'active': True, 'data': None}}
print(data['user']['active'])
# Output: True
print(type(data))
# Output: <class 'dict'>
Handle Deeply Nested Structures
import ast
complex_string = """
{
'level1': {
'level2': {
'level3': {
'value': [1, 2, 3],
'flag': True
}
}
}
}
"""
data = ast.literal_eval(complex_string)
print(data['level1']['level2']['level3']['value'])
# Output: [1, 2, 3]
ast.literal_eval() safely evaluates strings containing Python literals (strings, numbers, tuples, lists, dicts, booleans, and None). It cannot execute arbitrary code, making it safe for untrusted input.
Parse JSON Strings with json.loads()
When the string uses double quotes and JSON keywords (true, false, null), use the json module.
import json
json_string = '{"user": {"id": 1, "active": true, "data": null}}'
data = json.loads(json_string)
print(data)
# Output: {'user': {'id': 1, 'active': True, 'data': None}}
print(data['user']['active'])
# Output: True
Parse JSON API Responses
import json
api_response = '''
{
"status": "success",
"data": {
"users": [
{"id": 1, "profile": {"name": "Alice", "verified": true}},
{"id": 2, "profile": {"name": "Bob", "verified": false}}
]
}
}
'''
data = json.loads(api_response)
for user in data['data']['users']:
name = user['profile']['name']
verified = user['profile']['verified']
print(f"{name}: {'✓' if verified else '✗'}")
Output:
Alice: ✓
Bob: ✗
Identify the Correct Format
Examine the string to determine which parser to use.
| Indicator | Format | Parser |
|---|---|---|
Single quotes ('key') | Python | ast.literal_eval() |
Double quotes ("key") | JSON | json.loads() |
True / False | Python | ast.literal_eval() |
true / false | JSON | json.loads() |
None | Python | ast.literal_eval() |
null | JSON | json.loads() |
Auto-Detect and Parse
import ast
import json
def parse_nested_dict(s: str) -> dict:
"""Parse string to dict, auto-detecting format."""
s = s.strip()
# Try JSON first (more common in APIs)
try:
return json.loads(s)
except json.JSONDecodeError:
pass
# Fall back to Python literal
try:
result = ast.literal_eval(s)
if isinstance(result, dict):
return result
raise ValueError("String does not represent a dictionary")
except (ValueError, SyntaxError) as e:
raise ValueError(f"Cannot parse string as dict: {e}")
# Works with JSON
json_str = '{"nested": {"key": true}}'
print(parse_nested_dict(json_str))
# Output: {'nested': {'key': True}}
# Works with Python literals
python_str = "{'nested': {'key': True}}"
print(parse_nested_dict(python_str))
# Output: {'nested': {'key': True}}
Avoid the Quote Replacement Trap
Never attempt to convert between formats by replacing quotes. This approach fails with embedded quotes in string values.
import json
# ❌ BROKEN: Simple quote replacement
python_string = "{'message': \"It's working\"}"
# This creates invalid syntax
broken = python_string.replace("'", '"')
print(broken)
# Output: {"message": "It"s working"}
try:
json.loads(broken)
except json.JSONDecodeError as e:
print(f"Parse failed: {e}")
# Output: Parse failed: Expecting ',' delimiter: line 1 column 17 (char 16)
# ✅ CORRECT: Use ast for Python literals
import ast
data = ast.literal_eval(python_string)
print(data)
# Output: {'message': "It's working"}
Quote replacement fails with apostrophes, escaped characters, and nested quotes. Always use the appropriate parser for the format.
Handle Parsing Errors
Implement robust error handling for production code.
import ast
import json
def safe_parse_dict(s: str, default: dict = None) -> dict:
"""Safely parse nested dict string with fallback."""
if default is None:
default = {}
if not s or not s.strip():
return default
# Try JSON
try:
result = json.loads(s)
return result if isinstance(result, dict) else default
except (json.JSONDecodeError, TypeError):
pass
# Try Python literal
try:
result = ast.literal_eval(s)
return result if isinstance(result, dict) else default
except (ValueError, SyntaxError):
pass
return default
# Valid input
print(safe_parse_dict('{"a": {"b": 1}}'))
# Output: {'a': {'b': 1}}
# Invalid input returns default
print(safe_parse_dict('not a dict'))
# Output: {}
print(safe_parse_dict('invalid', {'fallback': True}))
# Output: {'fallback': True}
Access Nested Values Safely
Once parsed, access deeply nested values with error handling.
def get_nested(data: dict, *keys, default=None):
"""Safely get nested dictionary value."""
for key in keys:
try:
data = data[key]
except (KeyError, TypeError, IndexError):
return default
return data
config = {
'database': {
'primary': {
'host': 'localhost',
'port': 5432
}
}
}
# Safe access
print(get_nested(config, 'database', 'primary', 'host'))
# Output: localhost
# Missing key returns default
print(get_nested(config, 'database', 'backup', 'host', default='N/A'))
# Output: N/A
Quick Reference
| String Format | Parser | Example |
|---|---|---|
| Python literals | ast.literal_eval(s) | "{'key': True}" |
| JSON | json.loads(s) | '{"key": true}' |
| Unknown format | Try both with error handling | See parse_nested_dict() |
Conclusion
Use ast.literal_eval() for Python-formatted strings from logs, configuration files, or debugging output. Use json.loads() for JSON from web APIs or data interchange. Never attempt manual quote replacement: it creates subtle bugs with special characters. When the format is unknown, implement a parser that tries both methods with proper error handling.