pytest-dev/pytest · error · ValueError

is not a normalized and relative path

Error message

{basename} is not a normalized and relative path

What it means

TempPathFactory.mktemp() builds a subdirectory under basetemp from a caller-supplied basename. _ensure_relative_to_basetemp() normalizes the name then asserts that (basetemp/basename).resolve().parent == basetemp — i.e. the resolved path stays directly under basetemp. Absolute paths and any name that escapes via '..' fail this check, blocking path traversal and writes outside the temp tree.

Solutions

  1. Pass a plain relative name with no separators or `..` segments: 'foo', 'foo-', 'subdir'.
  2. Sanitize before calling: strip leading '/', reject any '..' component, prefer a single path component.
  3. If you need a nested layout, create the parent with mktemp first and join inside it yourself; do not push nesting into the basename.

Example fix

# before
sub = factory.mktemp(f"{user_supplied}/data")  # user_supplied may be '../x'

# after
import re
safe = re.sub(r"[^A-Za-z0-9_.-]", "_", user_supplied) or "tmp"
sub = factory.mktemp(safe)
Defensive patterns

Strategy: validation

Validate before calling

import os

def safe_basename(name: str) -> str:
    """Sanitize a caller-supplied name for TempPathFactory.mktemp()."""
    # Strip any absolute prefix and reject traversal.
    name = os.path.basename(os.path.normpath(name))
    if not name or name in (os.curdir, os.pardir) or '..' in name.split(os.sep):
        raise ValueError(f"unsafe basename for mktemp: {name!r}")
    return name

Type guard

import os

def is_safe_mktemp_basename(name: str, basetemp) -> bool:
    norm = os.path.normpath(name)
    return (
        not os.path.isabs(norm)
        and (basetemp / norm).resolve().parent == basetemp.resolve()
    )

Try / catch

from _pytest.tmpdir import TempPathFactory

def robust_mktemp(factory: TempPathFactory, name: str):
    try:
        return factory.mktemp(name)
    except ValueError:
        # Fall back to a sanitized single-component name.
        import re
        safe = re.sub(r"[^A-Za-z0-9_.-]", "_", name) or "tmp"
        return factory.mktemp(safe)

Prevention

When it happens

Trigger: Calling factory.mktemp('/abs/path'), factory.mktemp('../escape'), factory.mktemp('a/../../b'), or any basename whose normpath+resolve no longer sits immediately under basetemp. Reachable from internal callers (the tmp_path_factory fixture, the tmpdir factory) or plugin code that invokes mktemp directly.

Common situations: A plugin derives the basename from user input or test IDs and forgets to sanitize; a test passes an absolute path expecting mktemp to honor it; refactoring code that used to join paths manually.

Related errors


AI-assisted analysis of pytest-dev/pytest@0d6fbdeffa (2026-08-11). Data as JSON: /api/errors/acd042c01c55dc1d. Report an issue: GitHub.

Appendix: source

Thrown at src/_pytest/tmpdir.py:114

        if count < 0:
            raise ValueError(
                f"tmp_path_retention_count must be >= 0. Current input: {count}."
            )

        policy: RetentionType = config.getini("tmp_path_retention_policy")

        return cls(
            given_basetemp=config.option.basetemp,
            trace=config.trace.get("tmpdir"),
            retention_count=count,
            retention_policy=policy,
            _ispytest=True,
        )

    def _ensure_relative_to_basetemp(self, basename: str) -> str:
        basename = os.path.normpath(basename)
        if (self.getbasetemp() / basename).resolve().parent != self.getbasetemp():
            raise ValueError(f"{basename} is not a normalized and relative path")
        return basename

    def mktemp(self, basename: str, numbered: bool = True) -> Path:
        """Create a new temporary directory managed by the factory.

        :param basename:
            Directory base name, must be a relative path.

        :param numbered:
            If ``True``, ensure the directory is unique by adding a numbered
            suffix greater than any existing one: ``basename="foo-"`` and ``numbered=True``
            means that this function will create directories named ``"foo-0"``,
            ``"foo-1"``, ``"foo-2"`` and so on.

        :returns:
            The path to the new directory.
        """
        basename = self._ensure_relative_to_basetemp(basename)

View on GitHub (pinned to 0d6fbdeffa)