Skip to content

deeplabcut.core.config.utils

Centralized helpers for reading, writing, and creating configuration files (YAML).

Functions:

Name Description
create_config_template

Creates a template for config.yaml file. This specific order is preserved while saving as yaml file.

create_config_template_3d

Creates a template for config.yaml file for 3d project. This specific order is preserved while saving as yaml file.

edit_config

Convenience function to edit and save a config file from a dictionary.

ensure_plain_config

Convert typed config arguments into plain Python objects.

get_yaml_dumper

Get a ruamel.yaml YAML handler with representers for Enum and Path objects.

get_yaml_loader

Get a ruamel.yaml YAML handler with safe mode.

normalize_for_serialization

Recursively normalize Paths to strings and Enums to values.

pretty_print

Prints a model configuration in a pretty and readable way.

read_config

Reads structured config file defining a project.

read_config_as_dict

Args:

resolve_alias

Resolve a config key to its canonical field name.

resolve_aliases_in_dict

Rename deprecated config keys to their canonical names.

write_config

Writes a pose configuration file to disk.

write_config_3d

Write structured 3D project config file.

write_config_3d_template

Write 3D config from pre-built template and YAML instance.

write_project_config

Write structured project config file (config.yaml) preserving template order.

create_config_template

create_config_template(multianimal: bool = False) -> tuple

Creates a template for config.yaml file. This specific order is preserved while saving as yaml file.

Returns:

Type Description
tuple

(cfg_file, ruamelFile) for further editing and dumping.

Source code in deeplabcut/core/config/utils.py
@deprecated(replacement="deeplabcut.core.config.ProjectConfig", since="3.1")
def create_config_template(multianimal: bool = False) -> tuple:
    """
    Creates a template for config.yaml file. This specific order is preserved while saving as yaml file.

    Returns:
        (cfg_file, ruamelFile) for further editing and dumping.
    """
    from deeplabcut.core.config.project_config import ProjectConfig

    ruamelFile = get_yaml_dumper()

    # TODO @deruyter92 2026-06-15: This sentinel should be removed in v1.
    bodyparts = "MULTI!" if multianimal else []
    cfg_file = ProjectConfig(multianimalproject=multianimal, bodyparts=bodyparts).to_dict()
    return cfg_file, ruamelFile

create_config_template_3d

create_config_template_3d() -> tuple

Creates a template for config.yaml file for 3d project. This specific order is preserved while saving as yaml file.

Returns:

Type Description
tuple

(cfg_file_3d, ruamelFile_3d) for further editing and dumping.

Source code in deeplabcut/core/config/utils.py
def create_config_template_3d() -> tuple:
    """
    Creates a template for config.yaml file for 3d project. This specific order is preserved while saving as yaml file.

    Returns:
        (cfg_file_3d, ruamelFile_3d) for further editing and dumping.
    """
    yaml_str = """\
# Project definitions (do not edit)
Task:
scorer:
date:
\n
# Project path (change when moving around)
project_path:
\n
# Plotting configuration
skeleton: # Note that the pairs must be defined, as you want them linked!
skeleton_color:
pcutoff:
colormap:
dotsize:
alphaValue:
markerType:
markerColor:
\n
# Number of cameras, camera names, path of the config files, shuffle index and trainingsetindex used to analyze videos:
num_cameras:
camera_names:
scorername_3d: # Enter the scorer name for the 3D output
    """
    ruamelFile_3d = get_yaml_dumper()
    cfg_file_3d = ruamelFile_3d.load(yaml_str)
    return cfg_file_3d, ruamelFile_3d

edit_config

edit_config(configname: str | Path, edits: dict, output_name: str | Path = '') -> dict

Convenience function to edit and save a config file from a dictionary.

Note

Legacy helper without schema validation. Prefer manipulating and saving the typed config instead (e.g. cfg.update(edits).to_yaml(cfg_path))

Parameters

configname : string String containing the full path of the config file in the project. edits : dict Key–value pairs to edit in config output_name : string, optional (default='') Overwrite the original config.yaml by default. If passed in though, new filename of the edited config.

Examples

config_path = 'my_stellar_lab/dlc/config.yaml'

edits = {'numframes2pick': 5, 'trainingFraction': [0.5, 0.8], 'skeleton': [['a', 'b'], ['b', 'c']]}

deeplabcut.core.config.edit_config(config_path, edits)

Source code in deeplabcut/core/config/utils.py
def edit_config(configname: str | Path, edits: dict, output_name: str | Path = "") -> dict:
    """
    Convenience function to edit and save a config file from a dictionary.

    Note:
        Legacy helper without schema validation. Prefer manipulating and saving
        the typed config instead (e.g. cfg.update(edits).to_yaml(cfg_path))

    Parameters
    ----------
    configname : string
        String containing the full path of the config file in the project.
    edits : dict
        Key–value pairs to edit in config
    output_name : string, optional (default='')
        Overwrite the original config.yaml by default.
        If passed in though, new filename of the edited config.

    Examples
    --------
    config_path = 'my_stellar_lab/dlc/config.yaml'

    edits = {'numframes2pick': 5,
             'trainingFraction': [0.5, 0.8],
             'skeleton': [['a', 'b'], ['b', 'c']]}

    deeplabcut.core.config.edit_config(config_path, edits)
    """
    cfg = read_config_as_dict(configname)
    for key, value in edits.items():
        cfg[key] = value
    if not output_name:
        output_name = configname
    try:
        write_config(output_name, cfg)
    except ruamel.yaml.representer.RepresenterError:
        warnings.warn("Some edits could not be written. The configuration file will be left unchanged.", stacklevel=2)
        for key in edits:
            cfg.pop(key)
        write_config(output_name, cfg)
    return cfg

ensure_plain_config

ensure_plain_config(fn: Callable) -> Callable

Convert typed config arguments into plain Python objects.

Any positional or keyword argument that is a DLCBaseConfig is converted to a plain dict before the decorated function is called.

Source code in deeplabcut/core/config/utils.py
def ensure_plain_config(fn: Callable) -> Callable:
    """Convert typed config arguments into plain Python objects.

    Any positional or keyword argument that is a DLCBaseConfig is converted to
    a plain ``dict`` before the decorated function is called.
    """

    def _to_plain(value, fn_name: str = "<unknown>", var_name: str = "<unknown>"):
        # Lazy import to avoid circular imports during module initialization.
        from deeplabcut.core.config.base_config import DLCBaseConfig

        if isinstance(value, DLCBaseConfig):
            logger.debug(
                "converting %s (%s) to native dict in %s.",
                var_name,
                type(value).__name__,
                fn_name,
            )
            return value.to_dict()
        return value

    @wraps(fn)
    def wrapper(*args, **kwargs):
        fn_name = fn.__qualname__
        args = tuple(_to_plain(a, fn_name) for a in args)
        kwargs = {k: _to_plain(v, fn_name=fn_name, var_name=k) for k, v in kwargs.items()}
        return fn(*args, **kwargs)

    return wrapper

get_yaml_dumper

get_yaml_dumper() -> YAML

Get a ruamel.yaml YAML handler with representers for Enum and Path objects.

Source code in deeplabcut/core/config/utils.py
def get_yaml_dumper() -> YAML:
    """Get a ruamel.yaml YAML handler with representers for Enum and Path objects."""
    yaml = YAML(typ="rt", pure=True)

    # Use a very large width so long strings (e.g., file paths or keys with spaces)
    # are kept on a single line instead of being wrapped, which can otherwise cause
    # them to be emitted as complex keys. See also:
    # https://stackoverflow.com/questions/31197268/pyyaml-yaml-dump-produces-complex-key-for-string-key-122-chars/31199123#31199123
    # See PR https://github.com/DeepLabCut/DeepLabCut/pull/3140 for more details.
    yaml.width = 1_000_000

    # Auto-serialize Path objects as strings
    yaml.representer.add_multi_representer(PurePath, lambda r, p: r.represent_str(str(p)))
    yaml.representer.add_multi_representer(Enum, lambda r, e: r.represent_str(e.value))
    return yaml

get_yaml_loader

get_yaml_loader() -> YAML

Get a ruamel.yaml YAML handler with safe mode.

Source code in deeplabcut/core/config/utils.py
def get_yaml_loader() -> YAML:
    """Get a ruamel.yaml YAML handler with safe mode."""
    yaml = YAML(typ="safe", pure=True)
    return yaml

normalize_for_serialization

normalize_for_serialization(obj: Any) -> Any

Recursively normalize Paths to strings and Enums to values.

Source code in deeplabcut/core/config/utils.py
def normalize_for_serialization(obj: Any) -> Any:
    """Recursively normalize Paths to strings and Enums to values."""
    if isinstance(obj, Path):
        return str(obj)
    if isinstance(obj, Enum):
        return obj.value
    if isinstance(obj, Mapping):
        return type(obj)({k: normalize_for_serialization(v) for k, v in obj.items()})
    if isinstance(obj, tuple):
        return tuple(normalize_for_serialization(v) for v in obj)
    if isinstance(obj, (list, set)):
        return [normalize_for_serialization(v) for v in obj]
    if isinstance(obj, np.ndarray):
        return obj.tolist()
    return obj

pretty_print

pretty_print(config: dict, indent: int = 0, print_fn: Callable[[str], None] | None = None) -> None

Prints a model configuration in a pretty and readable way.

Parameters:

Name Type Description Default

config

dict

the config to print

required

indent

int

the base indent on all keys

0

print_fn

Callable[[str], None] | None

custom function to call (simply calls print if None)

None
Source code in deeplabcut/core/config/utils.py
def pretty_print(
    config: dict,
    indent: int = 0,
    print_fn: Callable[[str], None] | None = None,
) -> None:
    """Prints a model configuration in a pretty and readable way.

    Args:
        config: the config to print
        indent: the base indent on all keys
        print_fn: custom function to call (simply calls ``print`` if None)
    """
    if print_fn is None:
        print_fn = print

    for k, v in config.items():
        if isinstance(v, dict):
            print_fn(f"{indent * ' '}{k}:")
            pretty_print(v, indent + 2, print_fn=print_fn)
        else:
            print_fn(f"{indent * ' '}{k}: {v}")

read_config

read_config(configname: str | Path, ignore_empty: bool = True) -> ProjectConfig

Reads structured config file defining a project.

Applies default values and repairs (engine, detector_snapshotindex, project_path) and writes back if needed.

Parameters:

Name Type Description Default

configname

str | Path

Path to the project configuration file (config.yaml).

required

ignore_empty

bool

If True, empty/None values in the YAML are ignored and dataclass defaults are used instead. If False, empty values represent None. Defaults to True.

True

Returns:

Type Description
ProjectConfig

The project configuration as a ProjectConfig instance (supports dict-like access).

Source code in deeplabcut/core/config/utils.py
def read_config(configname: str | Path, ignore_empty: bool = True) -> ProjectConfig:
    """
    Reads structured config file defining a project.

    Applies default values and repairs (engine, detector_snapshotindex, project_path)
    and writes back if needed.

    Args:
        configname: Path to the project configuration file (config.yaml).
        ignore_empty: If True, empty/None values in the YAML are ignored and
            dataclass defaults are used instead. If False, empty values represent None.
            Defaults to True.

    Returns:
        The project configuration as a ProjectConfig instance (supports dict-like access).
    """
    from deeplabcut.core.config.project_config import ProjectConfig

    path = Path(configname)
    project_config = ProjectConfig.from_yaml(path, ignore_empty=ignore_empty)

    # If necessary, ProjectConfig automatically updates its project path via _post_yaml_load_updates.
    # if that is the case (marked as dirty), we write the config back to the file.
    if "project_path" in project_config.dirty_fields:
        # NOTE @deruyter92 2026-02-02: copied old behaviour of writing the config
        # immediately back to the file after reading it. We should consider separating
        # the writing and reading instead of having inplace edits during reading.
        project_config.to_yaml(configname, log_changes=True, mark_clean=True)
    return project_config

read_config_as_dict

read_config_as_dict(config_path: str | Path) -> dict

Parameters:

Name Type Description Default

config_path

str | Path

the path to the configuration file to load

required

Returns:

Type Description
dict

The configuration file with pure Python classes

Raises:

Type Description
FileNotFoundError

if the config file does not exist

Source code in deeplabcut/core/config/utils.py
def read_config_as_dict(config_path: str | Path) -> dict:
    """
    Args:
        config_path: the path to the configuration file to load

    Returns:
        The configuration file with pure Python classes

    Raises:
        FileNotFoundError: if the config file does not exist
    """
    if not Path(config_path).exists():
        raise FileNotFoundError(f"Config {config_path} is not found. Please make sure that the file exists.")
    with open(config_path) as f:
        cfg = get_yaml_loader().load(f)
    if cfg is None:
        raise ValueError(f"Config {config_path} is empty or null.")
    if not isinstance(cfg, dict):
        raise ValueError(f"Config {config_path} must be a YAML mapping at the top level, got {type(cfg).__name__}.")
    return cfg

resolve_alias

resolve_alias(name: str, alias_map: dict[str, str], *, warn: bool = True, stacklevel: int = 3) -> str

Resolve a config key to its canonical field name. Args: name: Raw key name (alias or canonical). alias_map: {alias: canonical_name} for deprecated keys. warn: If True, emit :class:DLCDeprecationWarning when name is an alias. stacklevel: Passed to :func:warnings.warn for deprecation messages. Returns: Canonical field name, or name unchanged if it is not an alias.

Source code in deeplabcut/core/config/utils.py
def resolve_alias(
    name: str,
    alias_map: dict[str, str],
    *,
    warn: bool = True,
    stacklevel: int = 3,
) -> str:
    """Resolve a config key to its canonical field name.
    Args:
        name: Raw key name (alias or canonical).
        alias_map: ``{alias: canonical_name}`` for deprecated keys.
        warn: If True, emit :class:`DLCDeprecationWarning` when ``name`` is an alias.
        stacklevel: Passed to :func:`warnings.warn` for deprecation messages.
    Returns:
        Canonical field name, or ``name`` unchanged if it is not an alias.
    """
    canonical = alias_map.get(name, name)
    if warn and name in alias_map:
        from deeplabcut.core.deprecation import DLCDeprecationWarning

        warnings.warn(
            f"'{name}' is deprecated, use '{canonical}' instead.",
            DLCDeprecationWarning,
            stacklevel=stacklevel,
        )
    return canonical

resolve_aliases_in_dict

resolve_aliases_in_dict(
    cfg_dict: dict, alias_map: dict[str, str], *, target: str = "config", warn: bool = True, stacklevel: int = 3
) -> dict

Rename deprecated config keys to their canonical names.

Parameters:

Name Type Description Default

cfg_dict

dict

Raw configuration mapping (e.g. from YAML).

required

alias_map

dict[str, str]

{alias: canonical_name} for deprecated keys.

required

target

str

Config class name shown in errors.

'config'

stacklevel

int

Passed to :func:warnings.warn for deprecation messages.

3

Returns:

Type Description
dict

A new dict with alias keys replaced by canonical names. Unchanged if alias_map is empty.

Raises:

Type Description
TypeError

If multiple keys resolve to the same canonical field name

Source code in deeplabcut/core/config/utils.py
def resolve_aliases_in_dict(
    cfg_dict: dict,
    alias_map: dict[str, str],
    *,
    target: str = "config",
    warn: bool = True,
    stacklevel: int = 3,
) -> dict:
    """Rename deprecated config keys to their canonical names.

    Args:
        cfg_dict: Raw configuration mapping (e.g. from YAML).
        alias_map: ``{alias: canonical_name}`` for deprecated keys.
        target: Config class name shown in errors.
        stacklevel: Passed to :func:`warnings.warn` for deprecation messages.

    Returns:
        A new dict with alias keys replaced by canonical names. Unchanged if
        ``alias_map`` is empty.

    Raises:
        TypeError: If multiple keys resolve to the same canonical field name
        (e.g. an alias and its canonical name, or two aliases for one field).
    """
    if not alias_map:
        return cfg_dict

    def _raise_for_duplicates(raw_to_canonical: dict[str, str]):
        counts = Counter(raw_to_canonical.values())
        conflicts = [f"{raw} -> {canonical}" for raw, canonical in raw_to_canonical.items() if counts[canonical] > 1]
        if conflicts:
            raise TypeError(f"{target} received duplicate canonical field names: {', '.join(conflicts)}.")

    raw_to_canonical = {raw: resolve_alias(raw, alias_map, warn=warn, stacklevel=stacklevel + 1) for raw in cfg_dict}
    _raise_for_duplicates(raw_to_canonical)
    return {raw_to_canonical[raw]: v for raw, v in cfg_dict.items()}

write_config

write_config(config_path: str | Path, config: dict, overwrite: bool = True) -> None

Writes a pose configuration file to disk.

Parameters:

Name Type Description Default

config_path

str | Path

the path where the config should be saved

required

config

dict

the config to save

required

overwrite

bool

whether to overwrite the file if it already exists

True
Source code in deeplabcut/core/config/utils.py
def write_config(config_path: str | Path, config: dict, overwrite: bool = True) -> None:
    """Writes a pose configuration file to disk.

    Args:
        config_path: the path where the config should be saved
        config: the config to save
        overwrite: whether to overwrite the file if it already exists

    Raises:
        FileExistsError if overwrite=True and the file already exists
    """
    if not overwrite and Path(config_path).exists():
        raise FileExistsError(f"Cannot write to {config_path} - set overwrite=True to force")

    with open(config_path, "w") as file:
        get_yaml_dumper().dump(config, file)

write_config_3d

write_config_3d(configname: str | Path, cfg: dict) -> None

Write structured 3D project config file.

Source code in deeplabcut/core/config/utils.py
def write_config_3d(configname: str | Path, cfg: dict) -> None:
    """Write structured 3D project config file."""
    with open(configname, "w") as cf:
        cfg_file, ruamelFile = create_config_template_3d()
        for key in cfg.keys():
            cfg_file[key] = cfg[key]
        ruamelFile.dump(cfg_file, cf)

write_config_3d_template

write_config_3d_template(projconfigfile: str | Path, cfg_file_3d: dict, ruamelFile_3d: YAML) -> None

Write 3D config from pre-built template and YAML instance.

Source code in deeplabcut/core/config/utils.py
def write_config_3d_template(projconfigfile: str | Path, cfg_file_3d: dict, ruamelFile_3d: YAML) -> None:
    """Write 3D config from pre-built template and YAML instance."""
    with open(projconfigfile, "w") as cf:
        ruamelFile_3d.dump(cfg_file_3d, cf)

write_project_config

write_project_config(configname: str | Path, cfg: dict | ProjectConfig) -> None

Write structured project config file (config.yaml) preserving template order.

Parameters:

Name Type Description Default

configname

str | Path

Path to the project configuration file (config.yaml).

required

cfg

dict | ProjectConfig

The project configuration to write (requires ProjectConfig schema).

required
Note

Validates before writing when possible, unvalidated legacy dump on failure; This may not round-trip via read_config for non-conformant legacy configurations.

Source code in deeplabcut/core/config/utils.py
def write_project_config(
    configname: str | Path,
    cfg: dict | ProjectConfig,
) -> None:
    """
    Write structured project config file (config.yaml) preserving template order.

    Args:
        configname (str | Path): Path to the project configuration file (config.yaml).
        cfg (dict | ProjectConfig): The project configuration to write (requires ProjectConfig schema).

    Note:
        Validates before writing when possible, unvalidated legacy dump on failure; This may not round-trip via
        read_config for non-conformant legacy configurations.
    """
    from deeplabcut.core.config.base_config import DLCBaseConfig
    from deeplabcut.core.config.project_config import ProjectConfig

    try:
        project_config: ProjectConfig = ProjectConfig.from_any(cfg)
        project_config.to_yaml(configname)
        return
    except ValidationError as e:
        logger.error(
            "Invalid configuration! Validation error in project config file %s. Error: %s "
            "Trying to write the file to disk anyway. Please verify the config file.",
            cfg,
            e,
        )
    if isinstance(cfg, DLCBaseConfig):
        logger.error("Expected a ProjectConfig, got %s.", type(cfg).__name__)
        cfg.to_yaml(configname)
        return

    warnings.warn("Reverting to legacy config file writing..", stacklevel=2)
    with open(configname, "w") as cf:
        cfg_file, ruamelFile = create_config_template(cfg.get("multianimalproject", False))
        for key in cfg.keys():
            cfg_file[key] = cfg[key]

        # Adding default value for variable skeleton and skeleton_color for backward compatibility.
        if "skeleton" not in cfg.keys():
            cfg_file["skeleton"] = []
            cfg_file["skeleton_color"] = "black"
        ruamelFile.dump(cfg_file, cf)