Skip to content

Interpolation & Resolvers

Interpolation

Reference config values and environment variables with ${...} syntax:

_target_: trainer
defaults:
  learning_rate: 0.01
  batch_size: 32

model:
  _target_: model
  lr: ${/defaults.learning_rate}     # Reference another config value
  scaled_lr: ${/defaults.learning_rate * 10}  # Arithmetic expressions

training:
  effective_batch: ${/defaults.batch_size * 4}
  output_dir: ${env:OUTPUT_DIR,./output}  # Environment variable with default
  user_path: /data/${env:USER}/runs       # Embedded in string

Config Path References

Syntax Example Description
Absolute ${/model.lr} From root of composed config
Relative ${./local.value} From current document root
Implicit ${model.lr} Same as relative
Parent ${../sibling.value} Parent-relative path

Environment Variables

# Required (raises error if not set)
data_path: ${env:DATA_PATH}

# With default value
log_level: ${env:LOG_LEVEL,INFO}
port: ${env:PORT,8080}  # Numbers are parsed
debug: ${env:DEBUG,false}  # Booleans are parsed

Expression Support

Full Python-like expression evaluation:

# Arithmetic
doubled: ${/learning_rate * 2}
ratio: ${/a / /b}
power: ${/base ** 2}

# Comparisons
is_large: ${/epochs > 100}
is_valid: ${/lr >= 0.001 and /lr <= 1.0}

# Boolean
enabled: ${/use_gpu and not /debug_mode}

# String concatenation
filename: ${"model_" + /name + "_v" + /version}

List Operations

items: [1, 2, 3, 4, 5]
callbacks: [logger, checkpoint, early_stop]

# Indexing and slicing
first: ${/items[0]}          # 1
last: ${/items[-1]}          # 5
subset: ${/items[1:3]}       # [2, 3]
from_start: ${/items[:3]}    # [1, 2, 3]
to_end: ${/items[2:]}        # [3, 4, 5]

# Concatenation
all: ${/base_list + /extra_list}

# Removal by value
filtered: ${/callbacks - ["early_stop"]}  # [logger, checkpoint]

# Remove by index
without_first: ${/items.remove(0)}  # [2, 3, 4, 5]

# Length
count: ${len(/items)}  # 5

# Membership
has_gpu: ${"gpu" in /devices}

Conditional Expressions

Ternary Operator

Conditional expressions using condition ? if_true : if_false syntax:

# Basic ternary with boolean
mode: '${/debug ? "verbose" : "quiet"}'

# With comparison
level: '${/count > 10 ? "high" : "low"}'

# With resolver as condition
status: '${app:is_ready() ? "go" : "wait"}'

# Nested ternary (right-associative)
result: '${/a ? "first" : /b ? "second" : "third"}'

Coalesce Operators

Two coalesce operators for handling null values and errors:

Operator Name Catches
?: Elvis (soft) None, missing resolver/env
?? Error (hard) None, missing resolver/env, all exceptions
# Elvis coalesce - catches null and missing
safe_id: '${app:uuid ?: "fallback-id"}'
env_val: '${env:API_KEY ?: "dev-key"}'

# Error coalesce - also catches exceptions
risky_value: '${app:might_fail() ?? "safe-default"}'

# Chained coalesce (right-associative)
value: '${app:primary() ?? app:backup() ?? "ultimate-fallback"}'

# Combined with ternary
result: '${(app:get_value() ?? 0) > 5 ? "high" : "low"}'

Elvis (?:) vs Error (??) Coalesce:

  • Use ?: when you want resolver exceptions to propagate (fail fast)
  • Use ?? when you want to catch all errors and use a fallback
# ?: propagates errors from app:risky
critical: '${app:risky() ?: "fallback"}'  # Raises if risky() throws

# ?? catches all errors
safe: '${app:risky() ?? "fallback"}'  # Returns "fallback" if risky() throws

Type Behavior

  • Standalone ${expr}: Preserves type (number stays number)
  • Embedded "text ${expr} more": Result is always string
# Standalone - type preserved
lr: ${/defaults.learning_rate}     # float: 0.01
count: ${len(/items)}               # int: 5
enabled: ${/use_gpu}                # bool: true

# Embedded - becomes string
message: "Learning rate is ${/defaults.learning_rate}"  # str: "Learning rate is 0.01"

Circular Reference Detection

Circular references are detected and raise CircularInterpolationError:

# This will raise an error
a: ${/b}
b: ${/a}  # Circular: a -> b -> a

Custom Resolvers

Register Python functions that can be called from interpolation expressions using the app: prefix:

import rconfig as rc
from datetime import datetime
import uuid

# Simple resolver (no arguments)
@rc.resolver("uuid")
def gen_uuid() -> str:
    return str(uuid.uuid4())

# Resolver with arguments
@rc.resolver("now")
def now(fmt: str = "%Y-%m-%d") -> str:
    return datetime.now().strftime(fmt)

# Namespaced resolver - all three syntaxes are equivalent:
@rc.resolver("db", "lookup")      # Multiple arguments
@rc.resolver("db:lookup")         # Colon-delimited string
@rc.resolver("db.lookup")         # Dot-delimited string
def db_lookup(table: str, id: int) -> dict:
    return database.get(table, id)

# Deeply nested namespace
@rc.resolver("db", "cache", "get")  # or "db:cache:get" or "db.cache.get"
def cache_get(key: str, ttl: int = 60) -> Any:
    return cache.get(key, ttl=ttl)

# Resolver with config access (special _config_ parameter)
@rc.resolver("derive")
def derive(path: str, *, _config_: dict) -> Any:
    return _config_.get(path)

Resolver Syntax

_target_: experiment

# No arguments (parentheses optional)
id: '${app:uuid}'
id_alt: '${app:uuid()}'  # Also valid

# Positional arguments
timestamp: '${app:now("%Y-%m-%d_%H-%M-%S")}'

# Keyword arguments
timestamp2: '${app:now(fmt="%Y-%m-%d")}'

# Namespaced resolver with arguments
user: '${app:db:lookup("users", 42)}'

# Deep namespace with kwargs
cached: '${app:db:cache:get("session", ttl=300)}'

# Config reference as argument
base_lr: 0.01
scaled_lr: '${app:scale(/base_lr, 2)}'

# Expression as argument
doubled: '${app:math:multiply(/base_lr, 2)}'

Config Access in Resolvers

Resolvers can access the full config by declaring a _config_ keyword-only parameter:

@rc.resolver("derive")
def derive(key: str, *, _config_: dict) -> Any:
    """Access config values from within a resolver."""
    return _config_.get(key)

The _config_ parameter receives a read-only view of the raw config dictionary (before instantiation).

Note: Resolvers can be used with conditional expressions (ternary ?:, coalesce ?: and ??). See Conditional Expressions for details.

Unregistering Resolvers

# Unregister a resolver
rc.unregister_resolver("uuid")
rc.unregister_resolver("db", "lookup")  # Namespaced