Katkat

Expand Recursive Key-Value Placeholders in a Template

GoogleSoftware Engineer · Technical Screen

Last updated

Implement a template engine that recursively resolves variable placeholders using a key-value dictionary while handling circular dependencies, missing keys, and escaped characters.

Problem Statement

You are given a template string template and a dictionary of key-value pairs mapping. Your goal is to resolve and replace all placeholders in template with their corresponding expanded values.

Placeholders follow these rules:

  • A placeholder is denoted by %KEY%, where KEY consists of one or more alphanumeric characters or underscores ([A-Za-z0-9_]+).
  • Values in mapping may themselves contain placeholders (e.g., %ANOTHER_KEY%), which must be recursively expanded.
  • Two consecutive percent characters %% denote an escaped literal % character and should not be parsed as placeholder delimiters.
  • If a circular dependency is detected among keys during expansion (e.g., A refers to B and B refers to A), raise a ValueError.
  • If a placeholder references a key that does not exist in mapping, raise a KeyError.

Return the fully expanded string.

Function Signature

def expand_template(template: str, mapping: dict[str, str]) -> str:
    pass

Constraints

  • 0 <= len(template) <= 10^4
  • 0 <= len(mapping) <= 1000
  • Keys in mapping consist of [A-Za-z0-9_]+
  • All strings consist of printable ASCII characters

Examples

Example 1

template = "I am %USER%. My home is %HOME%."
mapping = {
    "USER": "admin",
    "HOME": "/%USER%/home"
}
output = expand_template(template, mapping)
# Result: "I am admin. My home is /admin/home."

Explanation: %USER% expands to "admin". %HOME% expands to "/%USER%/home", which recursively resolves to "/admin/home".

Example 2

template = "Usage: 100%% completed by %USER%"
mapping = {
    "USER": "alice"
}
output = expand_template(template, mapping)
# Result: "Usage: 100% completed by alice"

Explanation: %% is escaped to a single literal %.

Example 3 (Cycle)

template = "Config: %A%"
mapping = {
    "A": "%B%",
    "B": "%A%"
}
# Raises ValueError: Circular dependency detected

Edge Cases to Consider

  • Unmatched or single % characters that do not form a valid placeholder should be treated as literal text.
  • Deeply nested or chain-like expansions (e.g., A -> B -> C -> ... -> Z).
  • Repeated references to the same key; expansion should avoid redundant work through memoization.

Hints

Hint 1

Treat the keys in mapping as nodes in a directed dependency graph and use a three-color depth-first search (unvisited, visiting, visited) to detect cycles.

Hint 2

Memoize the fully resolved string for each key so you only resolve each entry in mapping at most once.

Hint 3

Handle escaping (`%%`) carefully during tokenization or parsing so it does not interfere with the detection of placeholder boundaries.

Follow-up questions

  • How would you adapt your solution if the template is an unbounded stream of text rather than an in-memory string?
  • How would you support default fallback values if a key is not found (for example, `%KEY:-default%`)?
  • If the dependency chain is thousands of levels deep, how would you prevent recursion stack overflow errors?